2024-02-14 16:36:01 -03:00
{% extends "base.html" %}
2025-02-25 18:32:35 -03:00
2025-02-25 14:49:01 -03:00
{% block page_custom_head %}
2025-02-25 14:50:22 -03:00
< style >
.peer-extra-info {
display: none;
}
2025-02-25 18:32:35 -03:00
.callout.position-relative {
padding: 0 !important;
}
2025-02-26 11:27:54 -03:00
@keyframes blink {
2025-02-26 13:10:27 -03:00
50% { opacity: 0; }
2025-02-26 11:27:54 -03:00
}
.blinking-icon {
2025-02-26 13:10:27 -03:00
animation: blink 1s step-start infinite;
2025-02-26 11:27:54 -03:00
}
2025-02-28 18:32:40 -03:00
#inviteTextContainer {
border: 1px solid #ccc;
padding: 10px;
margin-bottom: 10px;
background-color: #f9f9f9;
}
2025-03-01 17:02:19 -03:00
#inviteText {
white-space: pre-line;
text-align: left;
}
2025-02-25 14:50:22 -03:00
< / style >
2025-02-25 14:49:01 -03:00
{% endblock %}
2024-02-14 16:36:01 -03:00
{% block content %}
2025-02-24 09:55:52 -03:00
{% if wireguard_instances %}
< div class = "card card-primary card-outline" >
< div class = "card-body" >
< ul class = "nav nav-tabs" role = "tablist" >
{% for wgconf in wireguard_instances %}
< li class = "nav-item" >
< a class = "nav-link {% if wgconf == current_instance %}active{% endif %}" href = "/peer/list/?uuid={{ wgconf.uuid }}" role = "tab" >
wg{{ wgconf.instance_id }} {% if wgconf.name %}({{ wgconf.name }}){% endif %}
< / a >
< / li >
{% endfor %}
< / ul >
< div class = "tab-content" id = "custom-content-below-tabContent" >
< div class = "tab-pane fade show active" id = "custom-content-below-home" role = "tabpanel" aria-labelledby = "custom-content-below-home-tab" >
2025-02-24 12:40:13 -03:00
< div class = "row" style = "padding-top: 15px" >
2025-02-24 09:55:52 -03:00
{% for peer in peer_list %}
2025-02-26 11:27:54 -03:00
< div class = "col-xl-6" id = "peer-{{ peer.public_key }}" data-uuid = "{{ peer.uuid }}" >
2025-02-25 18:32:35 -03:00
< div class = "callout position-relative" >
{% comment %}background: linear-gradient(to right, white 50%, transparent 50%);{% endcomment %}
2025-02-28 18:32:40 -03:00
< div class = "position-absolute p-3 div-peer-text-information" style = "top: 0; left: 0; background: linear-gradient(to right, white, transparent); width: 100%; height: 100%;" >
2025-02-25 18:32:35 -03:00
< div class = "d-flex justify-content-between align-items-start" >
2025-02-26 11:27:54 -03:00
< h5 id = "peer-name-{{ peer.public_key }}" >
2025-02-25 18:32:35 -03:00
< a href = "#" onclick = "openPeerModal('{{ peer.uuid }}');" style = "text-decoration: none" >
{{ peer }}
< / a >
< / h5 >
< span >
2025-02-28 18:32:40 -03:00
{% if user_acl.user_level >= 30 %}
< div class = "d-inline-flex flex-column" >
< a href = "/peer/sort/?peer={{ peer.uuid }}&direction=up" style = "line-height:0px" >
< i class = "fas fa-sort-up" > < / i >
< / a >
< div style = "overflow:hidden;margin-top: -9px" >
< a href = "/peer/sort/?peer={{ peer.uuid }}&direction=down" style = "position:relative;top:-11px" >
< i class = "fas fa-sort-down" > < / i >
< / a >
< / div >
< / div >
{% endif %}
< / span >
2025-02-25 18:32:35 -03:00
< / div >
2025-02-25 14:49:01 -03:00
< b class = "peer-extra-info" > Throughput: < / b > < span id = "peer-throughput-{{ peer.public_key }}" > < / span > < br >
< span class = "peer-extra-info" > < b > Transfer:< / b > < span id = "peer-transfer-{{ peer.public_key }}" > < / span > < br > < / span >
< span class = "peer-extra-info" > < b > Latest Handshake:< / b > < span id = "peer-latest-handshake-{{ peer.public_key }}" > < / span > < / span >
< span class = "peer-extra-info" > < span style = "display: none;" id = "peer-stored-latest-handshake-{{ peer.public_key }}" > {% if peer.peerstatus.last_handshake %}{{ peer.peerstatus.last_handshake|date:"U" }}{% else %}0{% endif %}< / span > < br > < / span >
< span class = "peer-extra-info" > < b > Endpoints:< / b > < span id = "peer-endpoints-{{ peer.public_key }}" > < / span > < br > < / span >
< span class = "peer-extra-info" id = "peer-extra-info-allowed-ips-{{ peer.public_key }}" >
2025-02-28 18:32:40 -03:00
< b > Allowed IPs:< / b >
< span id = "peer-allowed-ips-{{ peer.public_key }}" >
{% for address in peer.peerallowedip_set.all %}
{% if address.priority == 0 and address.config_file == 'server' %}
{{ address }}
{% endif %}
{% endfor %}
{% for address in peer.peerallowedip_set.all %}
{% if address.priority >= 1 and address.config_file == 'server' %}
{{ address }}
{% endif %}
{% endfor %}
< / span >
2025-02-25 14:49:01 -03:00
< / span >
2025-02-25 18:32:35 -03:00
< / div >
2025-02-26 11:27:54 -03:00
< canvas class = "" id = "chart-{{ peer.public_key }}" width = "800" height = "130" style = "min-height: 85px" > < / canvas >
2025-02-24 09:55:52 -03:00
< / div >
< / div >
{% endfor %}
2024-02-14 16:36:01 -03:00
< / div >
2025-02-24 09:55:52 -03:00
{% if add_peer_enabled %}
2025-02-24 12:40:13 -03:00
< a class = "btn btn-primary" href = "/peer/manage/?instance={{ current_instance.uuid }}" onclick = "return confirm('Are you sure you want to create a new peer?');" > Create Peer< / a >
2025-02-24 09:55:52 -03:00
{% else %}
< a class = "btn btn-primary disabled" href = "" > Create Peer< / a >
{% endif %}
2025-02-25 14:49:01 -03:00
< button id = "toggleExtraInfo" class = "btn btn-outline-primary" > Show extras< / button >
2024-02-14 16:36:01 -03:00
< / div >
< / div >
2025-02-24 09:28:42 -03:00
< / div >
2024-02-14 16:36:01 -03:00
< / div >
2025-02-24 09:55:52 -03:00
<!-- Peer Preview Modal -->
< div class = "modal fade" id = "peerPreviewModal" tabindex = "-1" aria-labelledby = "peerPreviewModalLabel" aria-hidden = "true" >
< div class = "modal-dialog modal-lg" >
< div class = "modal-content" >
< div class = "modal-header" >
< h5 class = "modal-title" id = "peerPreviewModalLabel" > Peer Preview< / h5 >
< button type = "button" class = "close" data-dismiss = "modal" aria-label = "Close" >
< span aria-hidden = "true" > × < / span >
< / button >
< / div >
< div class = "modal-body" >
2025-02-24 11:49:22 -03:00
<!-- Info content section -->
< div class = "info-content" >
2025-02-25 12:41:10 -03:00
< p > < b > < i class = "fas fa-arrows-alt-v nav-icon" > < / i > Throughput:< / b > < span id = "peerThroughput" > --< / span > < / p >
2025-02-24 11:49:22 -03:00
< p > < b > < i class = "fas fa-dolly nav-icon" > < / i > Transfer:< / b > < span id = "peerTransfer" > --< / span > < / p >
2025-03-01 09:53:21 -03:00
< p > < b > < i class = "far fa-clock nav-icon" > < / i > Latest Handshake:< / b > < span id = "peerHandshake" > --< / span > < / p >
2025-02-24 11:49:22 -03:00
< p > < b > < i class = "far fa-address-card nav-icon" > < / i > Endpoints:< / b > < span id = "peerEndpoints" > --< / span > < / p >
< p > < b > < i class = "fas fa-network-wired nav-icon" > < / i > Allowed IPs:< / b > < span id = "peerAllowedIPs" > --< / span > < / p >
2025-02-24 11:00:28 -03:00
2025-02-24 11:49:22 -03:00
<!-- Traffic Graph -->
< div class = "graph-container" style = "margin-top:20px;" >
< div class = "d-flex justify-content-between align-items-center" >
< label >
< i class = "fas fa-chart-area nav-icon" > < / i >
Peer Traffic
< / label >
< div class = "btn-group" role = "group" aria-label = "Graph interval" >
< a href = "#" data-period = "1h" class = "btn btn-outline-primary btn-xs" > 1h< / a >
< a href = "#" data-period = "3h" class = "btn btn-outline-primary btn-xs" > 3h< / a >
< a href = "#" data-period = "6h" class = "btn btn-outline-primary btn-xs" > 6h< / a >
< a href = "#" data-period = "1d" class = "btn btn-outline-primary btn-xs" > 1d< / a >
< a href = "#" data-period = "7d" class = "btn btn-outline-primary btn-xs" > 7d< / a >
< a href = "#" data-period = "30d" class = "btn btn-outline-primary btn-xs" > 1m< / a >
< a href = "#" data-period = "90d" class = "btn btn-outline-primary btn-xs" > 3m< / a >
< a href = "#" data-period = "180d" class = "btn btn-outline-primary btn-xs" > 6m< / a >
< a href = "#" data-period = "365d" class = "btn btn-outline-primary btn-xs" > 1y< / a >
< / div >
2025-02-24 11:00:28 -03:00
< / div >
2025-02-24 11:49:22 -03:00
< center style = "margin-top:10px;" >
< img id = "graphImg" src = "" class = "img-fluid" alt = "No traffic history, please wait a few minutes" style = "display:block;" >
< / center >
< / div >
< / div >
<!-- QR Code content section (initially hidden) -->
< div class = "qr-code-content" style = "display:none; " >
< button class = "btn btn-secondary" id = "backButton" > < i class = "fas fa-times" > < / i > Close QR Code< / button > < br >
< div style = "text-align: center;" >
< img id = "qrCodeImg" src = "" alt = "QR Code" class = "img-fluid" style = "max-width: 400px" / >
2025-02-24 11:00:28 -03:00
< / div >
2025-02-24 09:55:52 -03:00
< / div >
2025-02-28 18:32:40 -03:00
<!-- VPN Invite content section (initially hidden) -->
< div class = "invite-content" style = "display:none;" >
< button class = "btn btn-secondary" id = "backFromInviteButton" > < i class = "fas fa-arrow-left" > < / i > Back< / button > < br >
< div style = "text-align: center; margin-top: 10px;" >
< h5 > VPN Invite Details< / h5 >
<!-- Container com moldura para o texto do invite -->
< div id = "inviteTextContainer" >
< p id = "inviteText" > < / p >
< / div >
< p id = "invitePassword" > < / p >
2025-03-01 09:53:21 -03:00
< p >
Expires on: < span id = "inviteExpiration" > < / span >
< i class = "fas fa-sync-alt" id = "refreshInviteButton" style = "cursor: pointer;" title = "Refresh Invite" > < / i >
< / p >
2025-02-28 18:32:40 -03:00
< div class = "form-group" >
< label for = "inviteContactInput" > Enter Email or WhatsApp Number:< / label >
< input type = "text" class = "form-control" id = "inviteContactInput" placeholder = "Email or phone number" >
< / div >
< div >
2025-03-01 09:44:57 -03:00
< button class = "btn btn-outline-secondary" id = "copyInviteTextButton" > < i class = "fas fa-copy" > < / i > Copy Text< / button >
< button class = "btn btn-success" id = "sendInviteEmailButton" > < i class = "fas fa-envelope" > < / i > Email< / button >
< button class = "btn btn-success" id = "sendInviteWhatsappButton" > < i class = "fab fa-whatsapp" > < / i > WhatsApp< / button >
< button class = "btn btn-secondary" id = "closeInviteButton" > < i class = "far fa-trash-alt" > < / i > Delete< / button >
2025-02-28 18:32:40 -03:00
< / div >
< div id = "inviteMessage" style = "margin-top: 10px;" > < / div >
< / div >
< / div >
2025-02-24 09:55:52 -03:00
< / div >
< div class = "modal-footer" >
2025-02-24 11:49:22 -03:00
< button type = "button" class = "btn btn-secondary" data-dismiss = "modal" > < i class = "fas fa-times" > < / i > Close< / button >
< a href = "#" class = "btn btn-info" id = "downloadConfigButton" > < i class = "fas fa-download" > < / i > Config< / a >
< a href = "#" class = "btn btn-info" id = "qrcodeButton" > < i class = "fas fa-qrcode" > < / i > QR Code< / a >
2025-03-01 09:44:57 -03:00
< a href = "#" class = "btn btn-info" id = "inviteButton" > < i class = "fas fa-share" > < / i > VPN Invite< / a >
2025-02-24 11:49:22 -03:00
< a href = "#" class = "btn btn-outline-primary" id = "editPeerButton" > < i class = "far fa-edit" > < / i > Edit< / a >
2025-02-24 09:55:52 -03:00
< / div >
< / div >
< / div >
< / div >
2025-02-24 11:49:22 -03:00
2025-02-24 09:55:52 -03:00
{% else %}
< div class = "alert alert-warning" role = "alert" >
< h4 class = "alert-heading" > No WireGuard Instances Found< / h4 >
< p > There are no WireGuard instances configured. You can add a new instance by clicking the button below.< / p >
< / div >
< p >
< a href = "/server/manage/" class = "btn btn-primary" > Add WireGuard Instance< / a >
< / p >
{% endif %}
2025-02-24 09:28:42 -03:00
{% endblock %}
2024-02-14 16:36:01 -03:00
2025-02-24 09:28:42 -03:00
{% block custom_page_scripts %}
2025-02-25 15:34:26 -03:00
< script >
// Global object to store Chart.js instances for each peer.
var charts = {};
// Initialize charts for each peer once the DOM is ready.
document.addEventListener('DOMContentLoaded', function() {
// For each canvas element matching id pattern "chart-< peer_public_key > "
document.querySelectorAll('canvas[id^="chart-"]').forEach(function(canvas) {
var peerId = canvas.id.replace('chart-', '');
// Create a new Chart instance
charts[peerId] = new Chart(canvas.getContext('2d'), {
type: 'line',
data: {
// X-axis labels can be blank since we are only showing the last 10 points.
labels: Array(10).fill(''),
datasets: [
{
label: 'Download',
data: Array(10).fill(0),
borderColor: 'rgba(54, 162, 235, 1)',
backgroundColor: 'rgba(54, 162, 235, 0.2)',
fill: false,
2025-02-25 18:32:35 -03:00
tension: 0.1,
lineTension: 0.4,
pointRadius: 0
2025-02-25 15:34:26 -03:00
},
{
label: 'Upload',
data: Array(10).fill(0),
borderColor: 'rgba(255, 99, 132, 1)',
backgroundColor: 'rgba(255, 99, 132, 0.2)',
fill: false,
2025-02-25 18:32:35 -03:00
tension: 0.1,
lineTension: 0.4,
pointRadius: 0
2025-02-25 15:34:26 -03:00
}
]
},
options: {
2025-02-25 15:45:30 -03:00
legend: {
display: false
2025-02-25 15:34:26 -03:00
},
2025-02-25 15:45:30 -03:00
responsive: true,
2025-02-25 15:34:26 -03:00
scales: {
2025-02-25 15:45:30 -03:00
xAxes: [{
2025-02-25 15:34:26 -03:00
display: false,
2025-02-25 15:45:30 -03:00
ticks: { display: false },
gridLines: { display: false }
}],
yAxes: [{
display: false,
ticks: { display: false, beginAtZero: true },
gridLines: { display: false }
}]
2025-02-25 15:34:26 -03:00
},
animation: {
2025-02-25 18:32:35 -03:00
duration: 0
2025-02-25 15:34:26 -03:00
}
}
});
});
});
< / script >
2025-02-24 11:49:22 -03:00
< script >
document.addEventListener('DOMContentLoaded', function() {
$("#qrcodeButton").on("click", function(e) {
e.preventDefault();
var uuid = $("#peerPreviewModal").data("peer-uuid");
$("#qrCodeImg").attr("src", "/tools/download_peer_config/?uuid=" + uuid + "&format=qrcode");
$(".info-content").hide();
2025-02-28 18:32:40 -03:00
$(".invite-content").hide();
2025-02-24 11:49:22 -03:00
$(".qr-code-content").show();
});
$("#backButton").on("click", function(e) {
e.preventDefault();
$(".qr-code-content").hide();
$(".info-content").show();
});
});
< / script >
2025-02-24 09:55:52 -03:00
< script >
function openPeerModal(uuid) {
2025-02-24 11:49:22 -03:00
$(".qr-code-content").hide();
2025-02-28 18:32:40 -03:00
$(".invite-content").hide();
2025-02-24 11:49:22 -03:00
$(".info-content").show();
$("#qrCodeImg").attr("src", "");
2025-02-24 12:40:13 -03:00
$('#graphImg').attr('src', '').hide();
2025-02-24 11:00:28 -03:00
// Find the peer element by its data-uuid attribute
var peerElem = document.querySelector('[data-uuid="' + uuid + '"]');
if (peerElem) {
var peerNameFromCard = peerElem.querySelector('h5').innerText;
2025-02-25 14:20:50 -03:00
var peerThroughput = peerElem.querySelector('[id^="peer-throughput-"]').innerHTML;
2025-02-24 11:00:28 -03:00
var peerTransfer = peerElem.querySelector('[id^="peer-transfer-"]').innerText;
var peerHandshake = peerElem.querySelector('[id^="peer-latest-handshake-"]').innerText;
var peerEndpoints = peerElem.querySelector('[id^="peer-endpoints-"]').innerText;
2025-02-25 14:49:01 -03:00
var peerAllowedIPs = peerElem.querySelector('[id^="peer-allowed-ips-"]').innerHTML;
2024-02-14 16:36:01 -03:00
2025-02-24 11:00:28 -03:00
// Update the modal fields with the card values
$('#peerPreviewModalLabel').text(peerNameFromCard);
2025-02-25 14:20:50 -03:00
$('#peerThroughput').html(peerThroughput);
2025-02-24 11:00:28 -03:00
$('#peerTransfer').text(peerTransfer);
$('#peerHandshake').text(peerHandshake);
$('#peerEndpoints').text(peerEndpoints);
2025-02-25 14:49:01 -03:00
$('#peerAllowedIPs').html(peerAllowedIPs);
2025-02-24 11:00:28 -03:00
$('#editPeerButton').attr('href', '/peer/manage/?peer=' + uuid);
$('#downloadConfigButton').attr('href', '/tools/download_peer_config/?uuid=' + uuid);
$('#qrcodeButton').attr('href', '/tools/download_peer_config/?uuid=' + uuid + '&format=qrcode');
$('#graphImg').attr('src', '/rrd/graph/?peer=' + uuid).show();
$('#peerPreviewModal').data('peer-uuid', uuid);
2024-02-14 16:36:01 -03:00
2025-02-24 11:00:28 -03:00
$.ajax({
url: '/api/peer_info/',
data: { uuid: uuid },
type: 'GET',
dataType: 'json',
success: function(data) {
if (data.name) {
$('#peerPreviewModalLabel').text(data.name);
}
// Future additional peer information can be handled here.
},
error: function(xhr, status, error) {
console.error("Error fetching peer info:", error);
}
});
$('#peerPreviewModal').modal('show');
} else {
console.error('Peer element not found for uuid: ' + uuid);
}
2025-02-24 09:55:52 -03:00
}
2025-02-24 11:00:28 -03:00
$(document).on('click', '.graph-container .btn-group a', function(e) {
e.preventDefault();
var period = $(this).data('period');
var uuid = $('#peerPreviewModal').data('peer-uuid');
var newSrc = '/rrd/graph/?peer=' + uuid + '& period=' + period;
$('#graphImg').attr('src', newSrc);
});
2025-02-24 09:55:52 -03:00
< / script >
2024-02-17 11:53:51 -03:00
2025-02-24 09:55:52 -03:00
< script >
2025-02-25 14:50:22 -03:00
var previousMeasurements = {};
var toastShownThisCycle = false;
const updateThroughput = (peerId, peerInfo) => {
const throughputElement = document.getElementById(`peer-throughput-${peerId}`);
const currentTime = Date.now() / 1000; // current timestamp in seconds
let formattedThroughput = '';
if (previousMeasurements[peerId]) {
const prev = previousMeasurements[peerId];
const timeDiff = currentTime - prev.timestamp; // time difference in seconds
2025-02-26 13:10:27 -03:00
// For the peer: download corresponds to tx and upload to rx
2025-02-25 14:50:22 -03:00
let downloadDiff = peerInfo.transfer.tx - prev.transfer.tx;
let uploadDiff = peerInfo.transfer.rx - prev.transfer.rx;
2025-02-26 13:10:27 -03:00
// If counters have been reset (current value < previous ) , show a toast ( only once per cycle )
2025-02-25 14:50:22 -03:00
if (downloadDiff < 0 | | uploadDiff < 0 ) {
if (!toastShownThisCycle) {
$(document).Toasts('create', {
class: 'bg-info',
title: 'info',
body: 'Throughput discarded due to counter reset',
delay: 10000,
autohide: true
});
toastShownThisCycle = true;
}
downloadDiff = 0;
uploadDiff = 0;
2024-02-23 18:18:52 -03:00
}
2025-02-24 09:55:52 -03:00
2025-02-25 14:50:22 -03:00
// Calculate throughput in bytes per second
const downloadThroughput = downloadDiff / timeDiff;
const uploadThroughput = uploadDiff / timeDiff;
2025-02-25 14:20:50 -03:00
2025-02-26 13:10:27 -03:00
// Convert bytes per second to bits per second
const downloadBps = downloadThroughput * 8;
const uploadBps = uploadThroughput * 8;
// Calculate Mbps and Kbps values
const downloadMbps = downloadBps / 1000000;
const uploadMbps = uploadBps / 1000000;
const downloadKbps = downloadBps / 1000;
const uploadKbps = uploadBps / 1000;
// Determine display unit and formatting
let downloadDisplay, uploadDisplay;
if (downloadMbps < 1 ) {
// Below 1 Mbps, display in Kbps
if (downloadKbps < 100 ) {
downloadDisplay = downloadKbps.toFixed(2) + ' Kbps';
} else {
downloadDisplay = downloadKbps.toFixed(0) + ' Kbps';
}
} else {
// 1 Mbps and above: if above 10 Mbps, show no decimals; else, show 2 decimals
downloadDisplay = (downloadMbps > 10 ? downloadMbps.toFixed(0) : downloadMbps.toFixed(2)) + ' Mbps';
}
if (uploadMbps < 1 ) {
if (uploadKbps < 10 ) {
uploadDisplay = uploadKbps.toFixed(2) + ' Kbps';
} else {
uploadDisplay = uploadKbps.toFixed(0) + ' Kbps';
}
} else {
uploadDisplay = (uploadMbps > 10 ? uploadMbps.toFixed(0) : uploadMbps.toFixed(2)) + ' Mbps';
}
2025-02-25 14:20:50 -03:00
2025-02-26 13:10:27 -03:00
// Highlight values above a threshold
const threshold = 100.0;
if (downloadMbps > threshold) {
2025-02-25 14:50:22 -03:00
downloadDisplay = `< strong > ${downloadDisplay}< / strong > `;
}
2025-02-26 13:10:27 -03:00
if (uploadMbps > threshold) {
2025-02-25 14:50:22 -03:00
uploadDisplay = `< strong > ${uploadDisplay}< / strong > `;
}
formattedThroughput = `< i class = "fas fa-arrow-down" > < / i > ${downloadDisplay}, < i class = "fas fa-arrow-up" > < / i > ${uploadDisplay}`;
throughputElement.innerHTML = formattedThroughput;
2025-02-25 15:34:26 -03:00
2025-02-26 13:10:27 -03:00
// Update Chart.js graphs with raw Mbps values (always using Mbps for consistency)
2025-02-25 15:34:26 -03:00
if (charts[peerId]) {
var chart = charts[peerId];
2025-02-26 13:10:27 -03:00
chart.data.datasets[0].data.push(downloadMbps);
2025-02-25 15:34:26 -03:00
if (chart.data.datasets[0].data.length > 10) {
chart.data.datasets[0].data.shift();
}
2025-02-26 13:10:27 -03:00
chart.data.datasets[1].data.push(uploadMbps);
2025-02-25 15:34:26 -03:00
if (chart.data.datasets[1].data.length > 10) {
chart.data.datasets[1].data.shift();
}
chart.update();
}
2025-02-25 14:50:22 -03:00
} else {
// First cycle: no previous measurement available.
2025-02-26 13:26:57 -03:00
formattedThroughput = `< i class = "fas fa-arrow-down" > < / i > -.-- Kbps, < i class = "fas fa-arrow-up" > < / i > -.-- Kbps`;
2025-02-25 14:50:22 -03:00
throughputElement.innerHTML = formattedThroughput;
2025-02-25 14:20:50 -03:00
}
2025-02-25 14:50:22 -03:00
previousMeasurements[peerId] = {
timestamp: currentTime,
transfer: {
tx: peerInfo.transfer.tx,
rx: peerInfo.transfer.rx
}
};
2025-02-25 14:20:50 -03:00
2025-02-25 14:50:22 -03:00
return formattedThroughput;
2025-02-25 14:20:50 -03:00
};
2025-02-25 14:50:22 -03:00
// Convert bytes to human-readable format with abbreviated units
const convertBytes = (bytes) => {
if (bytes === 0) return '0 B';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
};
// Fetch Wireguard status and update UI
document.addEventListener('DOMContentLoaded', function() {
const fetchWireguardStatus = async () => {
try {
const response = await fetch('/api/wireguard_status/');
let data = await response.json();
// If latest-handshakes is 0, use the stored value
for (const [interfaceName, peers] of Object.entries(data)) {
for (const [peerId, peerInfo] of Object.entries(peers)) {
const peerElementId = `peer-stored-latest-handshake-${peerId}`;
const storedHandshakeElement = document.getElementById(peerElementId);
if (peerInfo['latest-handshakes'] === '0' & & storedHandshakeElement) {
peerInfo['latest-handshakes'] = storedHandshakeElement.textContent;
}
2025-02-25 14:20:50 -03:00
}
2025-02-24 09:55:52 -03:00
}
2025-02-25 14:50:22 -03:00
updateUI(data);
} catch (error) {
console.error('Error fetching Wireguard status:', error);
2024-02-17 11:53:51 -03:00
}
2025-02-25 14:50:22 -03:00
};
fetchWireguardStatus();
setInterval(fetchWireguardStatus, {{ current_instance.peer_list_refresh_interval }} * 1000);
});
2025-02-25 14:20:50 -03:00
2025-02-25 14:50:22 -03:00
const updateUI = (data) => {
// Reset the toast flag for this update cycle
toastShownThisCycle = false;
for (const [interfaceName, peers] of Object.entries(data)) {
for (const [peerId, peerInfo] of Object.entries(peers)) {
const peerDiv = document.getElementById(`peer-${peerId}`);
if (peerDiv) {
updatePeerInfo(peerDiv, peerId, peerInfo);
updateCalloutClass(peerDiv, peerInfo['latest-handshakes']);
// Calculate throughput and update the card
const throughputHTML = updateThroughput(peerId, peerInfo);
2025-02-28 18:32:40 -03:00
// If the modal is active for this peer, update its fields as well.
2025-02-25 14:50:22 -03:00
const peerUuid = peerDiv.getAttribute("data-uuid");
if ($('#peerPreviewModal').is(':visible') & & $('#peerPreviewModal').data('peer-uuid') === peerUuid) {
$('#peerThroughput').html(throughputHTML);
$('#peerTransfer').text(`${convertBytes(peerInfo.transfer.tx)} TX, ${convertBytes(peerInfo.transfer.rx)} RX`);
$('#peerHandshake').text(
peerInfo['latest-handshakes'] !== '0'
? new Date(parseInt(peerInfo['latest-handshakes']) * 1000).toLocaleString()
: '0'
);
$('#peerEndpoints').text(peerInfo.endpoints);
const allowedIpsModalElement = document.getElementById('peerAllowedIPs');
checkAllowedIps(allowedIpsModalElement, peerInfo['allowed-ips']);
}
}
}
2024-02-17 11:53:51 -03:00
}
2025-02-24 09:55:52 -03:00
};
2024-02-17 11:53:51 -03:00
2025-02-25 14:50:22 -03:00
const updatePeerInfo = (peerDiv, peerId, peerInfo) => {
const escapedPeerId = peerId.replace(/([!"#$%&'()*+,.\/:; < =>?@[\]^`{|}~])/g, '\\$1');
const transfer = peerDiv.querySelector(`#peer-transfer-${escapedPeerId}`);
const latestHandshake = peerDiv.querySelector(`#peer-latest-handshake-${escapedPeerId}`);
const endpoints = peerDiv.querySelector(`#peer-endpoints-${escapedPeerId}`);
const allowedIps = peerDiv.querySelector(`#peer-allowed-ips-${escapedPeerId}`);
transfer.textContent = `${convertBytes(peerInfo.transfer.tx)} TX, ${convertBytes(peerInfo.transfer.rx)} RX`;
latestHandshake.textContent = `${peerInfo['latest-handshakes'] !== '0' ? new Date(parseInt(peerInfo['latest-handshakes']) * 1000).toLocaleString() : '0'}`;
endpoints.textContent = `${peerInfo.endpoints}`;
checkAllowedIps(allowedIps, peerInfo['allowed-ips']);
};
const checkAllowedIps = (allowedIpsElement, allowedIpsApiResponse) => {
const apiIps = allowedIpsApiResponse[0].split(' ');
const htmlIpsText = allowedIpsElement.textContent.trim();
const htmlIpsArray = htmlIpsText.match(/\b(?:\d{1,3}\.){3}\d{1,3}\/\d{1,2}\b/g) || [];
allowedIpsElement.innerHTML = '';
2025-02-26 11:27:54 -03:00
let allowedIpsIssue = false;
2025-02-25 14:50:22 -03:00
htmlIpsArray.forEach((ip, index, array) => {
const ipSpan = document.createElement('span');
ipSpan.textContent = ip;
allowedIpsElement.appendChild(ipSpan);
if (!apiIps.includes(ip)) {
ipSpan.style.color = 'red';
ipSpan.style.textDecoration = 'underline';
ipSpan.title = 'This address does not appear in the wg show command output, likely indicating that another peer has an IP overlapping this network or that the configuration file is outdated.';
2025-02-26 11:27:54 -03:00
allowedIpsIssue = true;
2025-02-25 14:50:22 -03:00
}
if (index < array.length - 1 ) {
allowedIpsElement.appendChild(document.createTextNode(', '));
}
});
2025-02-26 11:27:54 -03:00
if (allowedIpsIssue) {
const peerId = allowedIpsElement.id.replace('peer-allowed-ips-', '');
const h5Element = document.getElementById('peer-name-' + peerId);
if (h5Element & & !h5Element.querySelector('.fa-exclamation-triangle')) {
const icon = document.createElement('i');
icon.className = 'fas fa-exclamation-triangle text-danger blinking-icon';
icon.title = 'At least one address does not appear in the wg show command output, which may indicate that another peer is using an overlapping IP or that the configuration file is outdated.';
h5Element.appendChild(icon);
2025-02-24 09:55:52 -03:00
}
2025-02-25 14:20:50 -03:00
}
2025-02-25 14:50:22 -03:00
};
2025-02-25 14:49:01 -03:00
2025-02-25 14:50:22 -03:00
const updateCalloutClass = (peerDiv, latestHandshake) => {
const calloutDiv = peerDiv.querySelector('.callout');
calloutDiv.classList.remove('callout-success', 'callout-info', 'callout-warning', 'callout-danger');
const handshakeAge = Date.now() / 1000 - parseInt(latestHandshake);
if (latestHandshake === '0') {
calloutDiv.classList.add('callout-danger');
} else if (handshakeAge < 600 ) {
calloutDiv.classList.add('callout-success');
} else if (handshakeAge < 1800 ) {
calloutDiv.classList.add('callout-info');
} else if (handshakeAge < 21600 ) {
calloutDiv.classList.add('callout-warning');
}
calloutDiv.style.transition = 'all 5s';
};
< / script >
2025-02-24 09:55:52 -03:00
2025-02-25 14:49:01 -03:00
< script >
$(document).ready(function(){
$("#toggleExtraInfo").click(function(){
$(".peer-extra-info").toggle();
if($(".peer-extra-info").is(":visible")){
$(this).text("Hide extras");
2025-02-28 18:32:40 -03:00
$(".div-peer-text-information").removeClass('position-absolute');
2025-02-25 14:49:01 -03:00
} else {
$(this).text("Show extras");
2025-02-28 18:32:40 -03:00
$(".div-peer-text-information").addClass('position-absolute');
2025-02-25 14:49:01 -03:00
}
});
});
< / script >
2025-02-28 18:32:40 -03:00
<!-- VPN Invite functionality with adjustments -->
< script >
$(document).ready(function(){
var inviteData = null; // Store invite details
// Function to detect mobile device
function isMobileDevice() {
return /Mobi|Android/i.test(navigator.userAgent);
}
// Handler for VPN Invite button click
$("#inviteButton").on("click", function(e) {
e.preventDefault();
var peerUuid = $('#peerPreviewModal').data('peer-uuid');
// Hide other content sections
$(".info-content").hide();
$(".qr-code-content").hide();
$(".invite-content").show();
// Clear previous invite message and input field
$("#inviteMessage").html("");
$("#inviteContactInput").val("");
// Create the invite by calling the API endpoint
$.ajax({
url: '/api/peer_invite/',
data: { peer: peerUuid },
type: 'GET',
dataType: 'json',
success: function(response) {
if(response.status === "success") {
inviteData = response.invite_data;
// Populate invite details in the modal
$("#inviteText").text(inviteData.text_body);
$("#invitePassword").html("Access Password: < strong > " + inviteData.password + "< / strong > (Share this password via a separate secure channel)");
$("#inviteExpiration").text(new Date(inviteData.expiration).toLocaleString());
} else {
2025-03-01 10:52:20 -03:00
$("#inviteMessage").html("< div class = 'alert alert-danger' > " + (response.message || "Unknown error") + "< / div > ");
2025-02-28 18:32:40 -03:00
}
},
error: function(xhr, status, error) {
2025-03-01 10:52:20 -03:00
var message = "Error creating invite.";
try {
var resp = xhr.responseJSON;
message = resp & & resp.message ? resp.message : xhr.statusText;
} catch(err) {
message = xhr.statusText;
}
$("#inviteMessage").html("< div class = 'alert alert-danger' > " + message + "< / div > ");
2025-02-28 18:32:40 -03:00
}
});
});
// Back button in the invite section
$("#backFromInviteButton").on("click", function(e) {
e.preventDefault();
$(".invite-content").hide();
$(".info-content").show();
});
// Validate email function
function isValidEmail(email) {
var re = /^\S+@\S+\.\S+$/;
return re.test(email);
}
// Validate phone number function (simple check)
function isValidPhone(phone) {
var re = /^\+?\d{10,15}$/;
return re.test(phone);
}
// Handler for copying the invite text to clipboard
$("#copyInviteTextButton").on("click", function(e) {
e.preventDefault();
var textToCopy = $("#inviteText").text();
if (navigator.clipboard) {
navigator.clipboard.writeText(textToCopy).then(function() {
$("#inviteMessage").html("< div class = 'alert alert-success' > Invite text copied to clipboard.< / div > ");
}, function(err) {
$("#inviteMessage").html("< div class = 'alert alert-danger' > Failed to copy text.< / div > ");
});
} else {
$("#inviteMessage").html("< div class = 'alert alert-danger' > Clipboard API not supported.< / div > ");
}
});
// Handler for sending invite via WhatsApp with device detection
$("#sendInviteWhatsappButton").on("click", function(e) {
e.preventDefault();
var contact = $("#inviteContactInput").val().trim();
if(!isValidPhone(contact)) {
$("#inviteMessage").html("< div class = 'alert alert-danger' > Please enter a valid phone number for WhatsApp.< / div > ");
return;
}
if(inviteData & & inviteData.whatsapp_body) {
var whatsappUrl;
if(isMobileDevice()){
whatsappUrl = "https://api.whatsapp.com/send?phone=" + encodeURIComponent(contact) + "& text=" + encodeURIComponent(inviteData.whatsapp_body);
} else {
whatsappUrl = "https://web.whatsapp.com/send?phone=" + encodeURIComponent(contact) + "& text=" + encodeURIComponent(inviteData.whatsapp_body);
}
window.open(whatsappUrl, '_blank');
} else {
$("#inviteMessage").html("< div class = 'alert alert-danger' > Invite data is not available.< / div > ");
}
});
2025-02-25 14:49:01 -03:00
2025-02-28 18:32:40 -03:00
// Handler for sending invite via Email
2025-03-01 10:52:20 -03:00
$("#sendInviteEmailButton").on("click", function(e, textStatus, xhr) {
2025-02-28 18:32:40 -03:00
e.preventDefault();
var contact = $("#inviteContactInput").val().trim();
if(!isValidEmail(contact)) {
$("#inviteMessage").html("< div class = 'alert alert-danger' > Please enter a valid email address.< / div > ");
return;
}
if(inviteData & & inviteData.uuid) {
// Send invite email via API call
$.ajax({
url: '/api/peer_invite/',
data: { invite: inviteData.uuid, action: 'email', address: contact },
type: 'GET',
dataType: 'json',
2025-03-01 10:52:20 -03:00
success: function(response, textStatus, xhr) {
var message = response.message;
if (!message) {
message = xhr.statusText;
}
2025-02-28 18:32:40 -03:00
if(response.status === "success") {
2025-03-01 10:52:20 -03:00
$("#inviteMessage").html("< div class = 'alert alert-success' > " + message + "< / div > ");
2025-02-28 18:32:40 -03:00
} else {
2025-03-01 10:52:20 -03:00
$("#inviteMessage").html("< div class = 'alert alert-danger' > " + message + "< / div > ");
2025-02-28 18:32:40 -03:00
}
},
error: function(xhr, status, error) {
2025-03-01 10:52:20 -03:00
var message = "Error sending email.";
try {
var resp = xhr.responseJSON;
if (resp & & resp.message) {
message = resp.message;
} else {
message = xhr.statusText;
}
} catch(err) {
message = xhr.statusText;
}
$("#inviteMessage").html("< div class = 'alert alert-danger' > " + message + "< / div > ");
2025-02-28 18:32:40 -03:00
}
});
} else {
$("#inviteMessage").html("< div class = 'alert alert-danger' > Invite data is not available.< / div > ");
}
});
2025-03-01 09:53:21 -03:00
// Handler for refreshing the invite (update expiration and content)
$("#refreshInviteButton").on("click", function(e) {
e.preventDefault();
if(inviteData & & inviteData.uuid) {
$.ajax({
url: '/api/peer_invite/',
data: { invite: inviteData.uuid, action: 'refresh' },
type: 'GET',
dataType: 'json',
success: function(response) {
if(response.status === "success") {
// Update the invite details
inviteData = response.invite_data;
$("#inviteText").text(inviteData.text_body);
$("#invitePassword").html("Access Password: < strong > " + inviteData.password + "< / strong > (Share this password via a separate secure channel)");
$("#inviteExpiration").text(new Date(inviteData.expiration).toLocaleString());
2025-03-01 10:52:20 -03:00
$("#inviteMessage").html("< div class = 'alert alert-success' > " + (response.message || xhr.statusText) + "< / div > ");
2025-03-01 09:53:21 -03:00
} else {
2025-03-01 10:52:20 -03:00
$("#inviteMessage").html("< div class = 'alert alert-danger' > " + (response.message || "Error refreshing invite.") + "< / div > ");
2025-03-01 09:53:21 -03:00
}
},
error: function(xhr, status, error) {
2025-03-01 10:52:20 -03:00
var message = "Error refreshing invite.";
try {
var resp = xhr.responseJSON;
if (resp & & resp.message) {
message = resp.message;
} else {
message = xhr.statusText;
}
} catch(err) {
message = xhr.statusText;
}
$("#inviteMessage").html("< div class = 'alert alert-danger' > " + message + "< / div > ");
2025-03-01 09:53:21 -03:00
}
});
} else {
$("#inviteMessage").html("< div class = 'alert alert-danger' > No invite data available to refresh.< / div > ");
}
});
2025-02-28 18:32:40 -03:00
// Handler for Close Invite button (which deletes the invite)
$("#closeInviteButton").on("click", function(e) {
e.preventDefault();
if(inviteData & & inviteData.uuid) {
$.ajax({
url: '/api/peer_invite/',
data: { invite: inviteData.uuid, action: 'delete' },
type: 'GET',
dataType: 'json',
success: function(response) {
// Hide invite section and show info content regardless of API response
$(".invite-content").hide();
$(".info-content").show();
inviteData = null;
},
error: function(xhr, status, error) {
$("#inviteMessage").html("< div class = 'alert alert-danger' > Error closing invite: " + error + "< / div > ");
}
});
} else {
$(".invite-content").hide();
$(".info-content").show();
}
});
});
< / script >
{% endblock %}