2024-02-14 16:36:01 -03:00
{% extends "base.html" %}
2025-04-14 17:21:57 -03:00
{% load i18n %}
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 1 s step-start infinite ;
2025-02-26 11:27:54 -03:00
}
2025-02-28 18:32:40 -03:00
# inviteTextContainer {
border : 1 px solid #ccc ;
padding : 10 px ;
margin-bottom : 10 px ;
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-04-14 17:21:57 -03:00
< b class = "peer-extra-info" > {% trans 'Throughput' %}: </ b > < span id = "peer-throughput-{{ peer.public_key }}" ></ span >< br >
< span class = "peer-extra-info" >< b > {% trans 'Transfer' %}:</ b > < span id = "peer-transfer-{{ peer.public_key }}" ></ span >< br ></ span >
< span class = "peer-extra-info" >< b > {% trans 'Latest Handshake' %}:</ b > < span id = "peer-latest-handshake-{{ peer.public_key }}" ></ span ></ span >
2025-02-25 14:49:01 -03:00
< 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 >
2025-04-14 17:21:57 -03:00
< span class = "peer-extra-info" >< b > {% trans 'Endpoints' %}:</ b > < span id = "peer-endpoints-{{ peer.public_key }}" ></ span >< br ></ span >
2025-02-25 14:49:01 -03:00
< span class = "peer-extra-info" id = "peer-extra-info-allowed-ips-{{ peer.public_key }}" >
2025-04-14 17:21:57 -03:00
< b > {% trans 'Allowed IPs' %}:</ b >
2025-02-28 18:32:40 -03:00
< 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-04-14 17:21:57 -03:00
< a class = "btn btn-primary" href = "/peer/manage/?instance={{ current_instance.uuid }}" onclick = "return confirm('{% trans 'Are you sure you want to create a new peer?' %}');" > {% trans 'Create Peer' %}</ a >
2025-02-24 09:55:52 -03:00
{% else %}
2025-04-14 17:21:57 -03:00
< a class = "btn btn-primary disabled" href = "" > {% trans 'Create Peer' %}</ a >
2025-02-24 09:55:52 -03:00
{% endif %}
2025-04-14 17:21:57 -03:00
< button id = "toggleExtraInfo" class = "btn btn-outline-primary" > {% trans '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-04-14 17:21:57 -03:00
< p >< b >< i class = "fas fa-arrows-alt-v nav-icon" ></ i > {% trans 'Throughput' %}:</ b > < span id = "peerThroughput" > --</ span ></ p >
< p >< b >< i class = "fas fa-dolly nav-icon" ></ i > {% trans 'Transfer' %}:</ b > < span id = "peerTransfer" > --</ span ></ p >
< p >< b >< i class = "far fa-clock nav-icon" ></ i > {% trans 'Latest Handshake' %}:</ b > < span id = "peerHandshake" > --</ span ></ p >
< p >< b >< i class = "far fa-address-card nav-icon" ></ i > {% trans 'Endpoints' %}:</ b > < span id = "peerEndpoints" > --</ span ></ p >
< p >< b >< i class = "fas fa-network-wired nav-icon" ></ i > {% trans '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 >
2025-04-14 17:21:57 -03:00
{% trans 'Peer Traffic' %}
2025-02-24 11:49:22 -03:00
</ 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;" >
2025-04-14 17:21:57 -03:00
< img id = "graphImg" src = "" class = "img-fluid" alt = "{% trans 'No traffic history, please wait a few minutes' %}" style = "display:block;" >
2025-02-24 11:49:22 -03:00
</ center >
</ div >
</ div >
<!-- QR Code content section (initially hidden) -->
< div class = "qr-code-content" style = "display:none; " >
2025-04-14 17:21:57 -03:00
< button class = "btn btn-secondary" id = "backButton" >< i class = "fas fa-times" ></ i > {% trans 'Close QR Code' %}</ button >< br >
2025-02-24 11:49:22 -03:00
< 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;" >
2025-04-14 17:21:57 -03:00
< button class = "btn btn-secondary" id = "backFromInviteButton" >< i class = "fas fa-arrow-left" ></ i > {% trans 'Back' %}</ button >< br >
2025-02-28 18:32:40 -03:00
< div style = "text-align: center; margin-top: 10px;" >
2025-04-14 17:21:57 -03:00
< h5 > {% trans 'VPN Invite Details' %}</ h5 >
2025-02-28 18:32:40 -03:00
< div id = "inviteTextContainer" >
< p id = "inviteText" ></ p >
</ div >
< p id = "invitePassword" ></ p >
2025-03-01 09:53:21 -03:00
< p >
2025-04-15 14:22:40 -03:00
{% trans 'Expires on' %}: < span id = "inviteExpiration" ></ span >
2026-01-07 12:11:35 -03:00
< i class = "fas fa-sync-alt" id = "refreshInviteButton" style = "cursor: pointer;" title = "{% trans 'Refresh Invite' %}" ></ i >
2025-03-01 09:53:21 -03:00
</ p >
2025-02-28 18:32:40 -03:00
< div class = "form-group" >
2025-04-14 17:21:57 -03:00
< label for = "inviteContactInput" > {% trans 'Enter Email or WhatsApp Number' %}:</ label >
< input type = "text" class = "form-control" id = "inviteContactInput" placeholder = "{% trans 'Email or phone number' %}" >
2025-02-28 18:32:40 -03:00
</ div >
< div >
2025-04-14 17:21:57 -03:00
< button class = "btn btn-outline-secondary" id = "copyInviteTextButton" >< i class = "fas fa-copy" ></ i > {% trans 'Copy Text' %}</ button >
< button class = "btn btn-success" id = "sendInviteEmailButton" >< i class = "fas fa-envelope" ></ i > {% trans 'Email' %}</ button >
< button class = "btn btn-success" id = "sendInviteWhatsappButton" >< i class = "fab fa-whatsapp" ></ i > {% trans 'WhatsApp' %}</ button >
< button class = "btn btn-secondary" id = "closeInviteButton" >< i class = "far fa-trash-alt" ></ i > {% trans '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-04-14 17:21:57 -03:00
< button type = "button" class = "btn btn-secondary" data-dismiss = "modal" >< i class = "fas fa-times" ></ i > {% trans 'Close' %}</ button >
< a href = "#" class = "btn btn-info" id = "downloadConfigButton" >< i class = "fas fa-download" ></ i > {% trans 'Config' %}</ a >
< a href = "#" class = "btn btn-info" id = "qrcodeButton" >< i class = "fas fa-qrcode" ></ i > {% trans 'QR Code' %}</ a >
< a href = "#" class = "btn btn-info" id = "inviteButton" >< i class = "fas fa-share" ></ i > {% trans 'VPN Invite' %}</ a >
< a href = "#" class = "btn btn-outline-primary" id = "editPeerButton" >< i class = "far fa-edit" ></ i > {% trans '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" >
2025-04-14 17:21:57 -03:00
< h4 class = "alert-heading" > {% trans 'No WireGuard Instances Found' %}</ h4 >
< p > {% trans 'There are no WireGuard instances configured. You can add a new instance by clicking the button below.' %}</ p >
2025-02-24 09:55:52 -03:00
</ div >
< p >
2025-04-14 17:21:57 -03:00
< a href = "/server/manage/" class = "btn btn-primary" > {% trans 'Add WireGuard Instance' %}</ a >
2025-02-24 09:55:52 -03:00
</ p >
{% endif %}
2024-02-14 16:36:01 -03:00
{% endblock %}
{% 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
responsive : true ,
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 ();
2025-08-12 11:44:34 -03:00
if ( $ ( this ). hasClass ( 'disabled' )) {
return false ;
}
2025-02-24 11:49:22 -03:00
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 ();
});
2025-08-12 11:44:34 -03:00
$ ( "#downloadConfigButton" ). on ( "click" , function ( e ) {
var hasPrivateKey = $ ( "#peerPreviewModal" ). data ( "has-private-key" );
if ( ! hasPrivateKey ) {
if ( ! confirm ( "{% trans 'This configuration does not contain a private key. You must add the private key manually in your client before using it.' %}" )) {
e . preventDefault ();
return false ;
}
}
});
2025-02-24 11:49:22 -03:00
});
</ 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 ;
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 );
$ . ajax ({
url : '/api/peer_info/' ,
data : { uuid : uuid },
type : 'GET' ,
dataType : 'json' ,
success : function ( data ) {
if ( data . name ) {
$ ( '#peerPreviewModalLabel' ). text ( data . name );
}
2025-08-12 11:44:34 -03:00
// Check if peer has private_key and enable/disable buttons accordingly
if ( ! data . private_key_exists ) {
$ ( '#qrcodeButton' ). addClass ( 'disabled' ). attr ( 'aria-disabled' , 'true' );
$ ( '#peerPreviewModal' ). data ( 'has-private-key' , false );
} else {
$ ( '#qrcodeButton' ). removeClass ( 'disabled' ). removeAttr ( 'aria-disabled' );
$ ( '#peerPreviewModal' ). data ( 'has-private-key' , true );
}
2025-02-24 11:00:28 -03:00
},
error : function ( xhr , status , error ) {
console . error ( "Error fetching peer info:" , error );
2024-02-23 18:18:52 -03:00
}
2025-02-24 11:00:28 -03:00
});
$ ( '#peerPreviewModal' ). modal ( 'show' );
} else {
console . error ( 'Peer element not found for uuid: ' + uuid );
}
2024-02-17 11:53:51 -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 );
});
2024-02-17 11:53:51 -03:00
</ script >
2025-02-24 09:55:52 -03:00
< script >
2025-02-25 14:50:22 -03:00
var previousMeasurements = {};
var toastShownThisCycle = false ;
2026-01-08 09:56:42 -03:00
const LOAD_FROM_CACHE = { % if load_from_cache % } true { % else % } false { % endif % };
2024-02-15 18:15:15 -03:00
2025-02-25 14:50:22 -03:00
const updateThroughput = ( peerId , peerInfo ) => {
const throughputElement = document . getElementById ( `peer-throughput- ${ peerId } ` );
const currentTime = Date . now () / 1000 ; // current timestamp in seconds
let formattedThroughput = '' ;
2025-02-25 14:20:50 -03:00
2025-02-25 14:50:22 -03:00
if ( previousMeasurements [ peerId ]) {
const prev = previousMeasurements [ peerId ];
const timeDiff = currentTime - prev . timestamp ; // time difference in seconds
2025-02-25 14:20:50 -03:00
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-25 14:20:50 -03:00
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 ;
2025-02-25 14:20:50 -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-26 13:10:27 -03:00
// Convert bytes per second to bits per second
const downloadBps = downloadThroughput * 8 ;
const uploadBps = uploadThroughput * 8 ;
2025-02-25 14:50:22 -03:00
2025-02-26 13:10:27 -03:00
// 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' ;
}
// 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 ];
};
2025-02-25 14:20:50 -03:00
2025-02-25 14:50:22 -03:00
// Fetch Wireguard status and update UI
document . addEventListener ( 'DOMContentLoaded' , function () {
2026-01-08 09:56:42 -03:00
const fetchWireguardStatus = async ( cachePrevious = null ) => {
2025-02-25 14:50:22 -03:00
try {
2026-01-08 09:56:42 -03:00
let url = '/api/wireguard_status/' ;
// cachePrevious === 0 should behave like "normal"
if ( cachePrevious !== null && cachePrevious !== 0 ) {
url += '?cache_previous=' + encodeURIComponent ( cachePrevious );
}
const response = await fetch ( url );
2025-02-25 14:50:22 -03:00
let data = await response . json ();
2025-02-25 14:20:50 -03:00
2025-02-25 14:50:22 -03:00
// 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-25 14:50:22 -03:00
updateUI ( data );
} catch ( error ) {
console . error ( 'Error fetching Wireguard status:' , error );
}
};
2026-01-08 09:56:42 -03:00
const primeFromCache = async () => {
for ( let i = {{ cache_previous_count }}; i >= 0 ; i -- ) {
await fetchWireguardStatus ( i );
}
};
( async () => {
if ( LOAD_FROM_CACHE ) {
await primeFromCache ();
} else {
await fetchWireguardStatus ();
}
setInterval ( fetchWireguardStatus , {{ refresh_interval }} * 1000 );
})();
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' ]);
2025-02-24 09:55:52 -03:00
}
}
}
2025-02-25 14:20:50 -03:00
}
};
2025-02-24 09:55:52 -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 } ` );
2025-02-25 14:20:50 -03:00
2025-02-25 14:50:22 -03:00
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' ]);
};
2025-02-25 14:20:50 -03:00
2025-02-25 14:50:22 -03:00
const checkAllowedIps = ( allowedIpsElement , allowedIpsApiResponse ) => {
2026-01-12 19:10:14 -03:00
const apiIps = Array . isArray ( allowedIpsApiResponse )
? allowedIpsApiResponse . filter ( Boolean )
: ( allowedIpsApiResponse || '' ). split ( /\s+/ ). filter ( Boolean );
2025-02-25 14:50:22 -03:00
const htmlIpsText = allowedIpsElement . textContent . trim ();
const htmlIpsArray = htmlIpsText . match ( /\b(?:\d{1,3}\.){3}\d{1,3}\/\d{1,2}\b/g ) || [];
2025-02-25 14:20:50 -03:00
2025-02-25 14:50:22 -03:00
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' ;
2025-04-14 17:21:57 -03:00
ipSpan . title = '{% trans ' 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' ;
2025-04-14 17:21:57 -03:00
icon . title = '{% trans ' 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 . ' %}' ;
2025-02-26 11:27:54 -03:00
h5Element . appendChild ( icon );
2025-02-24 09:55:52 -03:00
}
}
2025-02-25 14:50:22 -03:00
};
2025-02-24 09:55:52 -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 );
2025-02-25 14:20:50 -03:00
2025-02-25 14:50:22 -03:00
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" )){
2025-04-14 17:21:57 -03:00
$ ( this ). text ( "{% trans '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 {
2025-04-14 17:21:57 -03:00
$ ( this ). text ( "{% trans '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
2025-02-25 14:49:01 -03:00
2025-02-28 18:32:40 -03:00
// 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 );
2025-04-14 17:21:57 -03:00
$ ( "#invitePassword" ). html ( "{% trans 'Access Password' %}: <strong>" + inviteData . password + "</strong> {% trans '(Share this password via a separate secure channel)' %}" );
2025-02-28 18:32:40 -03:00
$ ( "#inviteExpiration" ). text ( new Date ( inviteData . expiration ). toLocaleString ());
} else {
2025-04-14 17:21:57 -03:00
$ ( "#inviteMessage" ). html ( "<div class='alert alert-danger'>" + ( response . message || "{% trans 'Unknown error' %}" ) + "</div>" );
2025-02-28 18:32:40 -03:00
}
},
error : function ( xhr , status , error ) {
2025-04-14 17:21:57 -03:00
var message = "{% trans 'Error creating invite.' %}" ;
2025-03-01 10:52:20 -03:00
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 () {
2025-04-14 17:21:57 -03:00
$ ( "#inviteMessage" ). html ( "<div class='alert alert-success'>{% trans 'Invite text copied to clipboard.' %}</div>" );
2025-02-28 18:32:40 -03:00
}, function ( err ) {
2025-04-14 17:21:57 -03:00
$ ( "#inviteMessage" ). html ( "<div class='alert alert-danger'>{% trans 'Failed to copy text.' %}</div>" );
2025-02-28 18:32:40 -03:00
});
} else {
2025-04-14 17:21:57 -03:00
$ ( "#inviteMessage" ). html ( "<div class='alert alert-danger'>{% trans 'Clipboard API not supported.' %}</div>" );
2025-02-28 18:32:40 -03:00
}
});
// Handler for sending invite via WhatsApp with device detection
$ ( "#sendInviteWhatsappButton" ). on ( "click" , function ( e ) {
e . preventDefault ();
var contact = $ ( "#inviteContactInput" ). val (). trim ();
if ( ! isValidPhone ( contact )) {
2025-04-14 17:21:57 -03:00
$ ( "#inviteMessage" ). html ( "<div class='alert alert-danger'>{% trans 'Please enter a valid phone number for WhatsApp.' %}</div>" );
2025-02-28 18:32:40 -03:00
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 {
2025-04-14 17:21:57 -03:00
$ ( "#inviteMessage" ). html ( "<div class='alert alert-danger'>{% trans 'Invite data is not available.' %}</div>" );
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 )) {
2025-04-14 17:21:57 -03:00
$ ( "#inviteMessage" ). html ( "<div class='alert alert-danger'>{% trans 'Please enter a valid email address.' %}</div>" );
2025-02-28 18:32:40 -03:00
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-04-14 17:21:57 -03:00
var message = "{% trans 'Error sending email.' %}" ;
2025-03-01 10:52:20 -03:00
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 {
2025-04-14 17:21:57 -03:00
$ ( "#inviteMessage" ). html ( "<div class='alert alert-danger'>{% trans 'Invite data is not available.' %}</div>" );
2025-02-28 18:32:40 -03:00
}
});
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 );
2025-04-14 17:21:57 -03:00
$ ( "#invitePassword" ). html ( "{% trans 'Access Password' %}: <strong>" + inviteData . password + "</strong> {% trans '(Share this password via a separate secure channel)' %}" );
2025-03-01 09:53:21 -03:00
$ ( "#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-04-14 17:21:57 -03:00
$ ( "#inviteMessage" ). html ( "<div class='alert alert-danger'>" + ( response . message || "{% trans 'Error refreshing invite.' %}" ) + "</div>" );
2025-03-01 09:53:21 -03:00
}
},
error : function ( xhr , status , error ) {
2025-04-14 17:21:57 -03:00
var message = "{% trans 'Error refreshing invite.' %}" ;
2025-03-01 10:52:20 -03:00
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 {
2025-04-14 17:21:57 -03:00
$ ( "#inviteMessage" ). html ( "<div class='alert alert-danger'>{% trans 'No invite data available to refresh.' %}</div>" );
2025-03-01 09:53:21 -03:00
}
});
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 ) {
2025-04-14 17:21:57 -03:00
$ ( "#inviteMessage" ). html ( "<div class='alert alert-danger'>{% trans 'Error closing invite:' %} " + error + "</div>" );
2025-02-28 18:32:40 -03:00
}
});
} else {
$ ( ".invite-content" ). hide ();
$ ( ".info-content" ). show ();
}
});
});
</ script >
{% endblock %}