From 9b1d2cabb93f4615cea0f08e64f8b89fb9687bee Mon Sep 17 00:00:00 2001
From: ReturnFI <151555003+ReturnFI@users.noreply.github.com>
Date: Wed, 24 Sep 2025 18:45:35 +0000
Subject: [PATCH] refactor: Externalize all template JavaScript to asset files
---
core/scripts/webpanel/assets/js/base.js | 138 +++
core/scripts/webpanel/assets/js/config.js | 95 ++
core/scripts/webpanel/assets/js/index.js | 73 ++
core/scripts/webpanel/assets/js/settings.js | 1193 +++++++++++++++++++
core/scripts/webpanel/assets/js/users.js | 73 ++
5 files changed, 1572 insertions(+)
create mode 100644 core/scripts/webpanel/assets/js/base.js
create mode 100644 core/scripts/webpanel/assets/js/config.js
create mode 100644 core/scripts/webpanel/assets/js/index.js
create mode 100644 core/scripts/webpanel/assets/js/settings.js
create mode 100644 core/scripts/webpanel/assets/js/users.js
diff --git a/core/scripts/webpanel/assets/js/base.js b/core/scripts/webpanel/assets/js/base.js
new file mode 100644
index 0000000..cae0bcd
--- /dev/null
+++ b/core/scripts/webpanel/assets/js/base.js
@@ -0,0 +1,138 @@
+$(function () {
+ const darkModeToggle = $("#darkModeToggle");
+ const darkModeIcon = $("#darkModeIcon");
+ const isDarkMode = localStorage.getItem("darkMode") === "enabled";
+
+ setDarkMode(isDarkMode);
+ updateIcon(isDarkMode);
+
+ darkModeToggle.on("click", function (e) {
+ e.preventDefault();
+ const enabled = $("body").hasClass("dark-mode");
+ localStorage.setItem("darkMode", enabled ? "disabled" : "enabled");
+ setDarkMode(!enabled);
+ updateIcon(!enabled);
+ });
+
+ function setDarkMode(enabled) {
+ $("body").toggleClass("dark-mode", enabled);
+
+ if (enabled) {
+ $(".main-header").addClass("navbar-dark").removeClass("navbar-light navbar-white");
+ $(".card").addClass("bg-dark");
+ } else {
+ $(".main-header").addClass("navbar-white navbar-light").removeClass("navbar-dark");
+ $(".card").removeClass("bg-dark");
+ }
+ }
+
+ function updateIcon(enabled) {
+ darkModeIcon.removeClass("fa-moon fa-sun")
+ .addClass(enabled ? "fa-sun" : "fa-moon");
+ }
+
+ const versionUrl = $('body').data('version-url');
+ $.ajax({
+ url: versionUrl,
+ type: 'GET',
+ success: function (response) {
+ $('#panel-version').text(`Version: ${response.current_version || 'N/A'}`);
+ },
+ error: function (error) {
+ console.error("Error fetching version:", error);
+ $('#panel-version').text('Version: Error');
+ }
+ });
+
+ function shouldCheckForUpdates() {
+ const lastCheck = localStorage.getItem('lastUpdateCheck');
+ const updateDismissed = localStorage.getItem('updateDismissed');
+ const now = Date.now();
+ const checkInterval = 24 * 60 * 60 * 1000;
+
+ if (!lastCheck) return true;
+ if (updateDismissed && now - parseInt(updateDismissed) < 2 * 60 * 60 * 1000) return false;
+
+ return now - parseInt(lastCheck) > checkInterval;
+ }
+
+ function showUpdateBar(version, changelog) {
+ $('#updateMessage').text(`Version ${version} is now available`);
+
+ const converter = new showdown.Converter();
+ const htmlChangelog = changelog ? converter.makeHtml(changelog) : '
No changelog available.
';
+ $('#changelogText').html(htmlChangelog);
+
+ $('#updateBar').slideDown(300);
+
+ $('#viewRelease').off('click').on('click', function(e) {
+ e.preventDefault();
+ window.open('https://github.com/ReturnFI/Blitz/releases/latest', '_blank');
+ });
+
+ $('#showChangelog').off('click').on('click', function() {
+ const $content = $('#changelogContent');
+ const $icon = $(this).find('i');
+
+ if ($content.is(':visible')) {
+ $content.slideUp(250);
+ $icon.removeClass('fa-chevron-up').addClass('fa-chevron-down');
+ $(this).css('opacity', '0.8');
+ } else {
+ $content.slideDown(250);
+ $icon.removeClass('fa-chevron-down').addClass('fa-chevron-up');
+ $(this).css('opacity', '1');
+ }
+ });
+
+ $('.dropdown-toggle').dropdown();
+
+ $('#remindLater').off('click').on('click', function(e) {
+ e.preventDefault();
+ $('#updateBar').slideUp(350);
+ });
+
+ $('#skipVersion').off('click').on('click', function(e) {
+ e.preventDefault();
+ localStorage.setItem('dismissedVersion', version);
+ localStorage.setItem('updateDismissed', Date.now().toString());
+ $('#updateBar').slideUp(350);
+ });
+
+ $('#closeUpdateBar').off('click').on('click', function() {
+ $('#updateBar').slideUp(350);
+ });
+ }
+
+ function checkForUpdates() {
+ if (!shouldCheckForUpdates()) return;
+
+ const checkVersionUrl = $('body').data('check-version-url');
+ $.ajax({
+ url: checkVersionUrl,
+ type: 'GET',
+ timeout: 10000,
+ success: function (response) {
+ localStorage.setItem('lastUpdateCheck', Date.now().toString());
+
+ if (response.is_latest) {
+ localStorage.removeItem('updateDismissed');
+ return;
+ }
+
+ const dismissedVersion = localStorage.getItem('dismissedVersion');
+ if (dismissedVersion === response.latest_version) return;
+
+ showUpdateBar(response.latest_version, response.changelog);
+ },
+ error: function (xhr, status, error) {
+ if (status !== 'timeout') {
+ console.warn("Update check failed:", error);
+ }
+ localStorage.setItem('lastUpdateCheck', Date.now().toString());
+ }
+ });
+ }
+
+ setTimeout(checkForUpdates, 2000);
+});
\ No newline at end of file
diff --git a/core/scripts/webpanel/assets/js/config.js b/core/scripts/webpanel/assets/js/config.js
new file mode 100644
index 0000000..84951cb
--- /dev/null
+++ b/core/scripts/webpanel/assets/js/config.js
@@ -0,0 +1,95 @@
+document.addEventListener('DOMContentLoaded', function () {
+ const mainContent = document.querySelector('.content-wrapper > div');
+ const GET_FILE_URL = mainContent.dataset.getFileUrl;
+ const SET_FILE_URL = mainContent.dataset.setFileUrl;
+
+ const saveButton = document.getElementById("save-button");
+ const restoreButton = document.getElementById("restore-button");
+ const container = document.getElementById("jsoneditor");
+
+ const editor = new JSONEditor(container, {
+ mode: "code",
+ onChange: validateJson
+ });
+
+ function validateJson() {
+ try {
+ editor.get();
+ updateSaveButton(true);
+ hideErrorMessage();
+ } catch (error) {
+ updateSaveButton(false);
+ showErrorMessage("Invalid JSON! Please correct the errors.");
+ }
+ }
+
+ function updateSaveButton(isValid) {
+ saveButton.disabled = !isValid;
+ saveButton.style.cursor = isValid ? "pointer" : "not-allowed";
+ saveButton.style.setProperty('background-color', isValid ? "#28a745" : "#ccc", 'important');
+ saveButton.style.setProperty('color', isValid ? "#fff" : "#666", 'important');
+ }
+
+ function showErrorMessage(message) {
+ Swal.fire({
+ title: "Error",
+ text: message,
+ icon: "error",
+ showConfirmButton: false,
+ timer: 5000,
+ position: 'top-right',
+ toast: true,
+ showClass: { popup: 'animate__animated animate__fadeInDown' },
+ hideClass: { popup: 'animate__animated animate__fadeOutUp' }
+ });
+ }
+
+ function hideErrorMessage() {
+ Swal.close();
+ }
+
+ function saveJson() {
+ Swal.fire({
+ title: 'Are you sure?',
+ text: 'Do you want to save the changes?',
+ icon: 'warning',
+ showCancelButton: true,
+ confirmButtonText: 'Yes, save it!',
+ cancelButtonText: 'Cancel',
+ reverseButtons: true
+ }).then((result) => {
+ if (result.isConfirmed) {
+ fetch(SET_FILE_URL, {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify(editor.get())
+ })
+ .then(() => {
+ Swal.fire('Saved!', 'Your changes have been saved.', 'success');
+ })
+ .catch(error => {
+ Swal.fire('Error!', 'There was an error saving your data.', 'error');
+ console.error("Error saving JSON:", error);
+ });
+ }
+ });
+ }
+
+ function restoreJson() {
+ fetch(GET_FILE_URL)
+ .then(response => response.json())
+ .then(json => {
+ editor.set(json);
+ Swal.fire('Success!', 'Your JSON has been loaded.', 'success');
+ })
+ .catch(error => {
+ Swal.fire('Error!', 'There was an error loading your JSON.', 'error');
+ console.error("Error loading JSON:", error);
+ });
+ }
+
+ saveButton.addEventListener('click', saveJson);
+ restoreButton.addEventListener('click', restoreJson);
+
+ restoreJson();
+});
\ No newline at end of file
diff --git a/core/scripts/webpanel/assets/js/index.js b/core/scripts/webpanel/assets/js/index.js
new file mode 100644
index 0000000..a8ec42e
--- /dev/null
+++ b/core/scripts/webpanel/assets/js/index.js
@@ -0,0 +1,73 @@
+function updateServerInfo() {
+ const serverStatusUrl = document.querySelector('.content').dataset.serverStatusUrl;
+ fetch(serverStatusUrl)
+ .then(response => response.json())
+ .then(data => {
+ document.getElementById('cpu-usage').textContent = data.cpu_usage;
+ document.getElementById('ram-usage').textContent = `${data.ram_usage} / ${data.total_ram}`;
+ document.getElementById('online-users').textContent = data.online_users;
+ document.getElementById('uptime').textContent = data.uptime;
+
+ document.getElementById('server-ipv4').textContent = `IPv4: ${data.server_ipv4 || 'N/A'}`;
+ document.getElementById('server-ipv6').textContent = `IPv6: ${data.server_ipv6 || 'N/A'}`;
+
+ document.getElementById('download-speed').textContent = `🔽 Download: ${data.download_speed}`;
+ document.getElementById('upload-speed').textContent = `🔼 Upload: ${data.upload_speed}`;
+ document.getElementById('tcp-connections').textContent = `TCP: ${data.tcp_connections}`;
+ document.getElementById('udp-connections').textContent = `UDP: ${data.udp_connections}`;
+
+ document.getElementById('reboot-uploaded-traffic').textContent = data.reboot_uploaded_traffic;
+ document.getElementById('reboot-downloaded-traffic').textContent = data.reboot_downloaded_traffic;
+ document.getElementById('reboot-total-traffic').textContent = data.reboot_total_traffic;
+
+ document.getElementById('user-uploaded-traffic').textContent = data.user_uploaded_traffic;
+ document.getElementById('user-downloaded-traffic').textContent = data.user_downloaded_traffic;
+ document.getElementById('user-total-traffic').textContent = data.user_total_traffic;
+ })
+ .catch(error => console.error('Error fetching server info:', error));
+}
+
+function updateServiceStatuses() {
+ const servicesStatusUrl = document.querySelector('.content').dataset.servicesStatusUrl;
+ fetch(servicesStatusUrl)
+ .then(response => response.json())
+ .then(data => {
+ updateServiceBox('hysteria2', data.hysteria_server);
+ updateServiceBox('telegrambot', data.hysteria_telegram_bot);
+ updateServiceBox('iplimit', data.hysteria_iplimit);
+ updateServiceBox('normalsub', data.hysteria_normal_sub);
+ })
+ .catch(error => console.error('Error fetching service statuses:', error));
+}
+
+function updateServiceBox(serviceName, status) {
+ const statusElement = document.getElementById(serviceName + '-status');
+ const statusBox = document.getElementById(serviceName + '-status-box');
+
+ if (status === true) {
+ statusElement.textContent = 'Active';
+ statusBox.classList.remove('bg-danger');
+ statusBox.classList.add('bg-success');
+ } else {
+ statusElement.textContent = 'Inactive';
+ statusBox.classList.remove('bg-success');
+ statusBox.classList.add('bg-danger');
+ }
+}
+
+document.addEventListener('DOMContentLoaded', function () {
+ updateServerInfo();
+ updateServiceStatuses();
+ setInterval(updateServerInfo, 2000);
+ setInterval(updateServiceStatuses, 10000);
+
+ const toggleIpBtn = document.getElementById('toggle-ip-visibility');
+ const ipAddressesDiv = document.getElementById('ip-addresses');
+ toggleIpBtn.addEventListener('click', function(e) {
+ e.preventDefault();
+ const isBlurred = ipAddressesDiv.style.filter === 'blur(5px)';
+ ipAddressesDiv.style.filter = isBlurred ? 'none' : 'blur(5px)';
+ toggleIpBtn.querySelector('i').classList.toggle('fa-eye');
+ toggleIpBtn.querySelector('i').classList.toggle('fa-eye-slash');
+ });
+});
\ No newline at end of file
diff --git a/core/scripts/webpanel/assets/js/settings.js b/core/scripts/webpanel/assets/js/settings.js
new file mode 100644
index 0000000..8cfcddb
--- /dev/null
+++ b/core/scripts/webpanel/assets/js/settings.js
@@ -0,0 +1,1193 @@
+$(document).ready(function () {
+ const contentSection = document.querySelector('.content');
+
+ const API_URLS = {
+ serverServicesStatus: contentSection.dataset.serverServicesStatusUrl,
+ getIp: contentSection.dataset.getIpUrl,
+ getPort: contentSection.dataset.getPortUrl,
+ getSni: contentSection.dataset.getSniUrl,
+ getAllNodes: contentSection.dataset.getAllNodesUrl,
+ addNode: contentSection.dataset.addNodeUrl,
+ deleteNode: contentSection.dataset.deleteNodeUrl,
+ getAllExtraConfigs: contentSection.dataset.getAllExtraConfigsUrl,
+ addExtraConfig: contentSection.dataset.addExtraConfigUrl,
+ deleteExtraConfig: contentSection.dataset.deleteExtraConfigUrl,
+ normalSubGetSubpath: contentSection.dataset.normalSubGetSubpathUrl,
+ telegramGetInterval: contentSection.dataset.telegramGetIntervalUrl,
+ getIpLimitConfig: contentSection.dataset.getIpLimitConfigUrl,
+ normalSubEditSubpath: contentSection.dataset.normalSubEditSubpathUrl,
+ setupDecoy: contentSection.dataset.setupDecoyUrl,
+ stopDecoy: contentSection.dataset.stopDecoyUrl,
+ getDecoyStatus: contentSection.dataset.getDecoyStatusUrl,
+ checkObfs: contentSection.dataset.checkObfsUrl,
+ enableObfs: contentSection.dataset.enableObfsUrl,
+ disableObfs: contentSection.dataset.disableObfsUrl,
+ telegramStart: contentSection.dataset.telegramStartUrl,
+ telegramStop: contentSection.dataset.telegramStopUrl,
+ telegramSetInterval: contentSection.dataset.telegramSetIntervalUrl,
+ normalSubStart: contentSection.dataset.normalSubStartUrl,
+ normalSubStop: contentSection.dataset.normalSubStopUrl,
+ setPortTemplate: contentSection.dataset.setPortUrlTemplate,
+ setSniTemplate: contentSection.dataset.setSniUrlTemplate,
+ editIp: contentSection.dataset.editIpUrl,
+ backup: contentSection.dataset.backupUrl,
+ restore: contentSection.dataset.restoreUrl,
+ startIpLimit: contentSection.dataset.startIpLimitUrl,
+ stopIpLimit: contentSection.dataset.stopIpLimitUrl,
+ configIpLimit: contentSection.dataset.configIpLimitUrl,
+ statusWarp: contentSection.dataset.statusWarpUrl,
+ updateGeoTemplate: contentSection.dataset.updateGeoUrlTemplate,
+ installWarp: contentSection.dataset.installWarpUrl,
+ uninstallWarp: contentSection.dataset.uninstallWarpUrl,
+ configureWarp: contentSection.dataset.configureWarpUrl
+ };
+
+ initUI();
+ fetchDecoyStatus();
+ fetchObfsStatus();
+ fetchNodes();
+ fetchExtraConfigs();
+
+ function escapeHtml(text) {
+ var map = {
+ '&': '&',
+ '<': '<',
+ '>': '>',
+ '"': '"',
+ "'": '''
+ };
+ if (text === null || typeof text === 'undefined') {
+ return '';
+ }
+ return String(text).replace(/[&<>"']/g, function(m) { return map[m]; });
+ }
+
+ function isValidURI(uri) {
+ if (!uri) return false;
+ const lowerUri = uri.toLowerCase();
+ return lowerUri.startsWith("vmess://") || lowerUri.startsWith("vless://") || lowerUri.startsWith("ss://") || lowerUri.startsWith("trojan://");
+ }
+
+ function isValidPath(path) {
+ if (!path) return false;
+ return path.trim() !== '';
+ }
+
+ function isValidDomain(domain) {
+ if (!domain) return false;
+ const lowerDomain = domain.toLowerCase();
+ return !lowerDomain.startsWith("http://") && !lowerDomain.startsWith("https://");
+ }
+
+ function isValidPort(port) {
+ if (!port) return false;
+ return /^[0-9]+$/.test(port) && parseInt(port) > 0 && parseInt(port) <= 65535;
+ }
+
+ function isValidSubPath(subpath) {
+ if (!subpath) return false;
+ return /^[a-zA-Z0-9]+$/.test(subpath);
+ }
+
+ function isValidIPorDomain(input) {
+ if (input === null || typeof input === 'undefined') return false;
+ input = input.trim();
+ if (input === '') return false;
+
+ const ipV4Regex = /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/;
+ const ipV6Regex = /^(([0-9a-fA-F]{1,4}:){7,7}([0-9a-fA-F]{1,4}|:)|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:))$/;
+ const domainRegex = /^(?!-)(?:[a-zA-Z\d-]{0,62}[a-zA-Z\d]\.){1,126}(?!\d+$)[a-zA-Z\d]{1,63}$/;
+ const lowerInput = input.toLowerCase();
+
+ return ipV4Regex.test(input) || ipV6Regex.test(input) || domainRegex.test(lowerInput);
+ }
+
+ function isValidPositiveNumber(value) {
+ if (!value) return false;
+ return /^[0-9]+$/.test(value) && parseInt(value) > 0;
+ }
+
+ function confirmAction(actionName, callback) {
+ Swal.fire({
+ title: `Are you sure?`,
+ text: `Do you really want to ${actionName}?`,
+ icon: "warning",
+ showCancelButton: true,
+ confirmButtonColor: "#3085d6",
+ cancelButtonColor: "#d33",
+ confirmButtonText: "Yes, proceed!",
+ cancelButtonText: "Cancel"
+ }).then((result) => {
+ if (result.isConfirmed) {
+ callback();
+ }
+ });
+ }
+
+ function sendRequest(url, type, data, successMessage, buttonSelector, showReload = true, postSuccessCallback = null) {
+ $.ajax({
+ url: url,
+ type: type,
+ contentType: "application/json",
+ data: data ? JSON.stringify(data) : null,
+ beforeSend: function() {
+ if (buttonSelector) {
+ $(buttonSelector).prop('disabled', true);
+ $(buttonSelector + ' .spinner-border').show();
+ }
+ },
+ success: function (response) {
+ Swal.fire("Success!", successMessage, "success").then(() => {
+ if (showReload) {
+ location.reload();
+ } else {
+ if (postSuccessCallback) {
+ postSuccessCallback(response);
+ }
+ }
+ });
+ },
+ error: function (xhr, status, error) {
+ let errorMessage = "An unexpected error occurred.";
+ if (xhr.responseJSON && xhr.responseJSON.detail) {
+ const detail = xhr.responseJSON.detail;
+ if (Array.isArray(detail)) {
+ errorMessage = detail.map(err => `Error in '${err.loc[1]}': ${err.msg}`).join('\n');
+ } else if (typeof detail === 'string') {
+ let userMessage = detail;
+ const failMarker = 'failed with exit code';
+ const markerIndex = detail.indexOf(failMarker);
+ if (markerIndex > -1) {
+ const colonIndex = detail.indexOf(':', markerIndex);
+ if (colonIndex > -1) {
+ userMessage = detail.substring(colonIndex + 1).trim();
+ }
+ }
+ errorMessage = userMessage;
+ }
+ }
+ Swal.fire("Error!", errorMessage, "error");
+ console.error("AJAX Error:", status, error, xhr.responseText);
+ },
+ complete: function() {
+ if (buttonSelector) {
+ $(buttonSelector).prop('disabled', false);
+ $(buttonSelector + ' .spinner-border').hide();
+ }
+ }
+ });
+ }
+
+ function validateForm(formId) {
+ let isValid = true;
+ $(`#${formId} .form-control:visible`).each(function () {
+ const input = $(this);
+ const id = input.attr('id');
+ let fieldValid = true;
+
+ if (id === 'normal_domain' || id === 'sni_domain' || id === 'decoy_domain') {
+ fieldValid = isValidDomain(input.val());
+ } else if (id === 'normal_port' || id === 'hysteria_port') {
+ fieldValid = isValidPort(input.val());
+ } else if (id === 'normal_subpath_input') {
+ fieldValid = isValidSubPath(input.val());
+ } else if (id === 'ipv4' || id === 'ipv6') {
+ fieldValid = (input.val().trim() === '') ? true : isValidIPorDomain(input.val());
+ } else if (id === 'node_ip') {
+ fieldValid = isValidIPorDomain(input.val());
+ } else if (id === 'node_name' || id === 'extra_config_name') {
+ fieldValid = input.val().trim() !== "";
+ } else if (id === 'extra_config_uri') {
+ fieldValid = isValidURI(input.val());
+ } else if (id === 'block_duration' || id === 'max_ips' || id === 'telegram_backup_interval') {
+ if (input.val().trim() === '' && id === 'telegram_backup_interval') {
+ fieldValid = true;
+ } else {
+ fieldValid = isValidPositiveNumber(input.val());
+ }
+ } else if (id === 'decoy_path') {
+ fieldValid = isValidPath(input.val());
+ } else {
+ if (input.attr('placeholder') && input.attr('placeholder').includes('Enter') && !input.attr('id').startsWith('ipv')) {
+ fieldValid = input.val().trim() !== "";
+ }
+ }
+
+ if (!fieldValid) {
+ input.addClass('is-invalid');
+ isValid = false;
+ } else {
+ input.removeClass('is-invalid');
+ }
+ });
+ return isValid;
+ }
+
+ function initUI() {
+ $.ajax({
+ url: API_URLS.serverServicesStatus,
+ type: "GET",
+ success: function (data) {
+ updateServiceUI(data);
+ },
+ error: function (xhr, status, error) {
+ console.error("Failed to fetch service status:", error, xhr.responseText);
+ Swal.fire("Error!", "Could not fetch service statuses.", "error");
+ }
+ });
+
+ $.ajax({
+ url: API_URLS.getIp,
+ type: "GET",
+ success: function (data) {
+ $("#ipv4").val(data.ipv4 || "");
+ $("#ipv6").val(data.ipv6 || "");
+ },
+ error: function (xhr, status, error) {
+ console.error("Failed to fetch IP addresses:", error, xhr.responseText);
+ }
+ });
+
+ $.ajax({
+ url: API_URLS.getPort,
+ type: "GET",
+ success: function (data) {
+ $("#hysteria_port").val(data.port || "");
+ },
+ error: function (xhr, status, error) {
+ console.error("Failed to fetch port:", error, xhr.responseText);
+ }
+ });
+
+ $.ajax({
+ url: API_URLS.getSni,
+ type: "GET",
+ success: function (data) {
+ $("#sni_domain").val(data.sni || "");
+ },
+ error: function (xhr, status, error) {
+ console.error("Failed to fetch SNI domain:", error, xhr.responseText);
+ }
+ });
+ }
+
+ function fetchNodes() {
+ $.ajax({
+ url: API_URLS.getAllNodes,
+ type: "GET",
+ success: function (nodes) {
+ renderNodes(nodes);
+ },
+ error: function(xhr) {
+ Swal.fire("Error!", "Failed to fetch external nodes list.", "error");
+ console.error("Error fetching nodes:", xhr.responseText);
+ }
+ });
+ }
+
+ function renderNodes(nodes) {
+ const tableBody = $("#nodes_table tbody");
+ tableBody.empty();
+
+ if (nodes && nodes.length > 0) {
+ $("#nodes_table").show();
+ $("#no_nodes_message").hide();
+ nodes.forEach(node => {
+ const row = `
+ | ${escapeHtml(node.name)} |
+ ${escapeHtml(node.ip)} |
+
+
+ |
+
`;
+ tableBody.append(row);
+ });
+ } else {
+ $("#nodes_table").hide();
+ $("#no_nodes_message").show();
+ }
+ }
+
+ function addNode() {
+ if (!validateForm('add_node_form')) return;
+
+ const name = $("#node_name").val().trim();
+ const ip = $("#node_ip").val().trim();
+
+ confirmAction(`add the node '${name}'`, function () {
+ sendRequest(
+ API_URLS.addNode,
+ "POST",
+ { name: name, ip: ip },
+ `Node '${name}' added successfully!`,
+ "#add_node_btn",
+ false,
+ function() {
+ $("#node_name").val('');
+ $("#node_ip").val('');
+ $("#add_node_form .form-control").removeClass('is-invalid');
+ fetchNodes();
+ }
+ );
+ });
+ }
+
+ function deleteNode(nodeName) {
+ confirmAction(`delete the node '${nodeName}'`, function () {
+ sendRequest(
+ API_URLS.deleteNode,
+ "POST",
+ { name: nodeName },
+ `Node '${nodeName}' deleted successfully!`,
+ null,
+ false,
+ fetchNodes
+ );
+ });
+ }
+
+ function fetchExtraConfigs() {
+ $.ajax({
+ url: API_URLS.getAllExtraConfigs,
+ type: "GET",
+ success: function (configs) {
+ renderExtraConfigs(configs);
+ },
+ error: function(xhr) {
+ Swal.fire("Error!", "Failed to fetch extra configurations.", "error");
+ console.error("Error fetching extra configs:", xhr.responseText);
+ }
+ });
+ }
+
+ function renderExtraConfigs(configs) {
+ const tableBody = $("#extra_configs_table tbody");
+ tableBody.empty();
+
+ if (configs && configs.length > 0) {
+ $("#extra_configs_table").show();
+ $("#no_extra_configs_message").hide();
+ configs.forEach(config => {
+ const shortUri = config.uri.length > 50 ? config.uri.substring(0, 50) + '...' : config.uri;
+ const row = `
+ | ${escapeHtml(config.name)} |
+ ${escapeHtml(shortUri)} |
+
+
+ |
+
`;
+ tableBody.append(row);
+ });
+ } else {
+ $("#extra_configs_table").hide();
+ $("#no_extra_configs_message").show();
+ }
+ }
+
+ function addExtraConfig() {
+ if (!validateForm('add_extra_config_form')) return;
+
+ const name = $("#extra_config_name").val().trim();
+ const uri = $("#extra_config_uri").val().trim();
+
+ confirmAction(`add the configuration '${name}'`, function () {
+ sendRequest(
+ API_URLS.addExtraConfig,
+ "POST",
+ { name: name, uri: uri },
+ `Configuration '${name}' added successfully!`,
+ "#add_extra_config_btn",
+ false,
+ function() {
+ $("#extra_config_name").val('');
+ $("#extra_config_uri").val('');
+ $("#add_extra_config_form .form-control").removeClass('is-invalid');
+ fetchExtraConfigs();
+ }
+ );
+ });
+ }
+
+ function deleteExtraConfig(configName) {
+ confirmAction(`delete the configuration '${configName}'`, function () {
+ sendRequest(
+ API_URLS.deleteExtraConfig,
+ "POST",
+ { name: configName },
+ `Configuration '${configName}' deleted successfully!`,
+ null,
+ false,
+ fetchExtraConfigs
+ );
+ });
+ }
+
+ function updateServiceUI(data) {
+ const servicesMap = {
+ "hysteria_telegram_bot": "#telegram_form",
+ "hysteria_normal_sub": "#normal_sub_service_form",
+ "hysteria_iplimit": "#ip-limit-service",
+ "hysteria_warp": "#warp_service"
+ };
+
+ Object.keys(servicesMap).forEach(serviceKey => {
+ let isRunning = data[serviceKey];
+
+ if (serviceKey === "hysteria_telegram_bot") {
+ const $form = $("#telegram_form");
+ if (isRunning) {
+ $form.find('[data-group="start-only"]').hide();
+ $("#telegram_start").hide();
+ $("#telegram_stop").show();
+ $("#telegram_save_interval").show();
+ if ($form.find(".alert-info").length === 0) {
+ $form.prepend(`Service is running. You can stop it or change the backup interval.
`);
+ }
+ fetchTelegramBackupInterval();
+ } else {
+ $form.find('[data-group="start-only"]').show();
+ $("#telegram_start").show();
+ $("#telegram_stop").hide();
+ $("#telegram_save_interval").hide();
+ $form.find(".alert-info").remove();
+ $("#telegram_backup_interval").val("");
+ }
+
+ } else if (serviceKey === "hysteria_normal_sub") {
+ const $normalForm = $("#normal_sub_service_form");
+ const $normalFormGroups = $normalForm.find(".form-group");
+ const $normalStartBtn = $("#normal_start");
+ const $normalStopBtn = $("#normal_stop");
+ const $normalSubConfigTabLi = $(".normal-sub-config-tab-li");
+
+ if (isRunning) {
+ $normalFormGroups.hide();
+ $normalStartBtn.hide();
+ $normalStopBtn.show();
+ if ($normalForm.find(".alert-info").length === 0) {
+ $normalForm.prepend(`NormalSub service is running. You can stop it or configure its subpath.
`);
+ }
+ $normalSubConfigTabLi.show();
+ fetchNormalSubPath();
+ } else {
+ $normalFormGroups.show();
+ $normalStartBtn.show();
+ $normalStopBtn.hide();
+ $normalForm.find(".alert-info").remove();
+ $normalSubConfigTabLi.hide();
+ if ($('#normal-sub-config-link-tab').hasClass('active')) {
+ $('#normal-tab').tab('show');
+ }
+ $("#normal_subpath_input").val("");
+ $("#normal_subpath_input").removeClass('is-invalid');
+ }
+ } else if (serviceKey === "hysteria_iplimit") {
+ const $ipLimitServiceForm = $("#ip_limit_service_form");
+ const $configTabLi = $(".ip-limit-config-tab-li");
+ if (isRunning) {
+ $("#ip_limit_start").hide();
+ $("#ip_limit_stop").show();
+ $configTabLi.show();
+ fetchIpLimitConfig();
+ if ($ipLimitServiceForm.find(".alert-info").length === 0) {
+ $ipLimitServiceForm.prepend(`IP-Limit service is running. You can stop it if needed.
`);
+ }
+ } else {
+ $("#ip_limit_start").show();
+ $("#ip_limit_stop").hide();
+ $configTabLi.hide();
+ if ($('#ip-limit-config-tab').hasClass('active')) {
+ $('#ip-limit-service-tab').tab('show');
+ }
+ $ipLimitServiceForm.find(".alert-info").remove();
+ $("#block_duration").val("");
+ $("#max_ips").val("");
+ $("#block_duration, #max_ips").removeClass('is-invalid');
+ }
+ } else if (serviceKey === "hysteria_warp") {
+ const isWarpServiceRunning = data[serviceKey];
+ if (isWarpServiceRunning) {
+ $("#warp_initial_controls").hide();
+ $("#warp_active_controls").show();
+ fetchWarpFullStatusAndConfig();
+ } else {
+ $("#warp_initial_controls").show();
+ $("#warp_active_controls").hide();
+ if ($("#warp_config_form").length > 0) {
+ $("#warp_config_form")[0].reset();
+ }
+ }
+ }
+ });
+ }
+
+ function fetchNormalSubPath() {
+ $.ajax({
+ url: API_URLS.normalSubGetSubpath,
+ type: "GET",
+ success: function (data) {
+ $("#normal_subpath_input").val(data.subpath || "");
+ if (data.subpath) {
+ $("#normal_subpath_input").removeClass('is-invalid');
+ }
+ },
+ error: function (xhr, status, error) {
+ console.error("Failed to fetch NormalSub subpath:", error, xhr.responseText);
+ $("#normal_subpath_input").val("");
+ }
+ });
+ }
+
+ function fetchTelegramBackupInterval() {
+ $.ajax({
+ url: API_URLS.telegramGetInterval,
+ type: "GET",
+ success: function (data) {
+ if (data.backup_interval) {
+ $("#telegram_backup_interval").val(data.backup_interval);
+ } else {
+ $("#telegram_backup_interval").val("");
+ }
+ },
+ error: function (xhr, status, error) {
+ console.error("Failed to fetch Telegram backup interval:", error, xhr.responseText);
+ $("#telegram_backup_interval").val("");
+ }
+ });
+ }
+
+ function fetchIpLimitConfig() {
+ $.ajax({
+ url: API_URLS.getIpLimitConfig,
+ type: "GET",
+ success: function (data) {
+ $("#block_duration").val(data.block_duration || "");
+ $("#max_ips").val(data.max_ips || "");
+ if (data.block_duration) $("#block_duration").removeClass('is-invalid');
+ if (data.max_ips) $("#max_ips").removeClass('is-invalid');
+ },
+ error: function (xhr, status, error) {
+ console.error("Failed to fetch IP Limit config:", error, xhr.responseText);
+ $("#block_duration").val("");
+ $("#max_ips").val("");
+ }
+ });
+ }
+
+ function editNormalSubPath() {
+ if (!validateForm('normal_sub_config_form')) return;
+ const subpath = $("#normal_subpath_input").val();
+
+ confirmAction("change the NormalSub subpath to '" + subpath + "'", function () {
+ sendRequest(
+ API_URLS.normalSubEditSubpath,
+ "PUT",
+ { subpath: subpath },
+ "NormalSub subpath updated successfully!",
+ "#normal_subpath_save_btn",
+ false,
+ fetchNormalSubPath
+ );
+ });
+ }
+
+ function setupDecoy() {
+ if (!validateForm('decoy_form')) return;
+ const domain = $("#decoy_domain").val();
+ const path = $("#decoy_path").val();
+ confirmAction("set up the decoy site", function () {
+ sendRequest(
+ API_URLS.setupDecoy,
+ "POST",
+ { domain: domain, decoy_path: path },
+ "Decoy site setup initiated successfully!",
+ "#decoy_setup",
+ false,
+ function() { setTimeout(fetchDecoyStatus, 1000); }
+ );
+ });
+ }
+
+ function stopDecoy() {
+ confirmAction("stop the decoy site", function () {
+ sendRequest(
+ API_URLS.stopDecoy,
+ "POST",
+ null,
+ "Decoy site stop initiated successfully!",
+ "#decoy_stop",
+ false,
+ function() { setTimeout(fetchDecoyStatus, 1000); }
+ );
+ });
+ }
+
+ function fetchDecoyStatus() {
+ $.ajax({
+ url: API_URLS.getDecoyStatus,
+ type: "GET",
+ success: function (data) {
+ updateDecoyStatusUI(data);
+ },
+ error: function (xhr, status, error) {
+ $("#decoy_status_message").html('Failed to fetch decoy status.
');
+ console.error("Failed to fetch decoy status:", error, xhr.responseText);
+ }
+ });
+ }
+
+ function updateDecoyStatusUI(data) {
+ const $form = $("#decoy_form");
+ const $formGroups = $form.find(".form-group");
+ const $setupBtn = $("#decoy_setup");
+ const $stopBtn = $("#decoy_stop");
+ const $alertInfo = $form.find(".alert-info");
+
+ if (data.active) {
+ $formGroups.hide();
+ $setupBtn.hide();
+ $stopBtn.show();
+ if ($alertInfo.length === 0) {
+ $form.prepend(`Decoy site is running. You can stop it if needed.
`);
+ } else {
+ $alertInfo.text('Decoy site is running. You can stop it if needed.');
+ }
+ $("#decoy_status_message").html(`
+ Status: Active
+ Path: ${data.path || 'N/A'}
+ `);
+ } else {
+ $formGroups.show();
+ $setupBtn.show();
+ $stopBtn.hide();
+ $alertInfo.remove();
+ $("#decoy_status_message").html('Status: Not Active');
+ }
+ }
+
+ function fetchObfsStatus() {
+ $.ajax({
+ url: API_URLS.checkObfs,
+ type: "GET",
+ success: function (data) {
+ updateObfsUI(data.obfs);
+ },
+ error: function (xhr, status, error) {
+ $("#obfs_status_message").html('Failed to fetch OBFS status.');
+ console.error("Failed to fetch OBFS status:", error, xhr.responseText);
+ $("#obfs_enable_btn").hide();
+ $("#obfs_disable_btn").hide();
+ }
+ });
+ }
+
+ function updateObfsUI(statusMessage) {
+ $("#obfs_status_message").text(statusMessage);
+ if (statusMessage === "OBFS is active.") {
+ $("#obfs_enable_btn").hide();
+ $("#obfs_disable_btn").show();
+ $("#obfs_status_container").removeClass("border-danger border-warning alert-danger alert-warning").addClass("border-success alert-success");
+ } else if (statusMessage === "OBFS is not active.") {
+ $("#obfs_enable_btn").show();
+ $("#obfs_disable_btn").hide();
+ $("#obfs_status_container").removeClass("border-success border-danger alert-success alert-danger").addClass("border-warning alert-warning");
+ } else {
+ $("#obfs_enable_btn").hide();
+ $("#obfs_disable_btn").hide();
+ $("#obfs_status_container").removeClass("border-success border-warning alert-success alert-warning").addClass("border-danger alert-danger");
+ }
+ }
+
+ function enableObfs() {
+ confirmAction("enable OBFS", function () {
+ sendRequest(
+ API_URLS.enableObfs,
+ "GET",
+ null,
+ "OBFS enabled successfully!",
+ "#obfs_enable_btn",
+ false,
+ fetchObfsStatus
+ );
+ });
+ }
+
+ function disableObfs() {
+ confirmAction("disable OBFS", function () {
+ sendRequest(
+ API_URLS.disableObfs,
+ "GET",
+ null,
+ "OBFS disabled successfully!",
+ "#obfs_disable_btn",
+ false,
+ fetchObfsStatus
+ );
+ });
+ }
+
+ function startTelegram() {
+ if (!validateForm('telegram_form')) return;
+ const apiToken = $("#telegram_api_token").val();
+ const adminId = $("#telegram_admin_id").val();
+ let backupInterval = $("#telegram_backup_interval").val();
+
+ const data = {
+ token: apiToken,
+ admin_id: adminId
+ };
+ if (backupInterval) {
+ data.backup_interval = parseInt(backupInterval);
+ }
+
+ confirmAction("start the Telegram bot", function () {
+ sendRequest(
+ API_URLS.telegramStart,
+ "POST",
+ data,
+ "Telegram bot started successfully!",
+ "#telegram_start"
+ );
+ });
+ }
+
+ function stopTelegram() {
+ confirmAction("stop the Telegram bot", function () {
+ sendRequest(
+ API_URLS.telegramStop,
+ "DELETE",
+ null,
+ "Telegram bot stopped successfully!",
+ "#telegram_stop"
+ );
+ });
+ }
+
+ function saveTelegramInterval() {
+ if (!validateForm('telegram_form')) return;
+ let backupInterval = $("#telegram_backup_interval").val();
+
+ if (!backupInterval) {
+ Swal.fire("Error!", "Backup interval cannot be empty.", "error");
+ return;
+ }
+
+ const data = {
+ backup_interval: parseInt(backupInterval)
+ };
+
+ confirmAction(`change the backup interval to ${backupInterval} hours`, function () {
+ sendRequest(
+ API_URLS.telegramSetInterval,
+ "POST",
+ data,
+ "Backup interval updated successfully!",
+ "#telegram_save_interval",
+ false,
+ fetchTelegramBackupInterval
+ );
+ });
+ }
+
+
+ function startNormal() {
+ if (!validateForm('normal_sub_service_form')) return;
+ const domain = $("#normal_domain").val();
+ const port = $("#normal_port").val();
+ confirmAction("start the normal subscription", function () {
+ sendRequest(
+ API_URLS.normalSubStart,
+ "POST",
+ { domain: domain, port: port },
+ "Normal subscription started successfully!",
+ "#normal_start"
+ );
+ });
+ }
+
+ function stopNormal() {
+ confirmAction("stop the normal subscription", function () {
+ sendRequest(
+ API_URLS.normalSubStop,
+ "DELETE",
+ null,
+ "Normal subscription stopped successfully!",
+ "#normal_stop"
+ );
+ });
+ }
+
+ function changePort() {
+ if (!validateForm('port_form')) return;
+ const port = $("#hysteria_port").val();
+ const url = API_URLS.setPortTemplate.replace("PORT_PLACEHOLDER", port);
+ confirmAction("change the port", function () {
+ sendRequest(url, "GET", null, "Port changed successfully!", "#port_change");
+ });
+ }
+
+ function changeSNI() {
+ if (!validateForm('sni_form')) return;
+ const domain = $("#sni_domain").val();
+ const url = API_URLS.setSniTemplate.replace("SNI_PLACEHOLDER", domain);
+ confirmAction("change the SNI", function () {
+ sendRequest(url, "GET", null, "SNI changed successfully!", "#sni_change");
+ });
+ }
+
+ function saveIP() {
+ if (!validateForm('change_ip_form')) return;
+ const ipv4 = $("#ipv4").val().trim() || null;
+ const ipv6 = $("#ipv6").val().trim() || null;
+ confirmAction("save the new IP settings", function () {
+ sendRequest(
+ API_URLS.editIp,
+ "POST",
+ { ipv4: ipv4, ipv6: ipv6 },
+ "IP settings saved successfully!",
+ "#ip_change"
+ );
+ });
+ }
+
+ function downloadBackup() {
+ window.location.href = API_URLS.backup;
+ Swal.fire("Starting Download", "Your backup download should start shortly.", "info");
+ }
+
+ function uploadBackup() {
+ var fileInput = document.getElementById('backup_file');
+ var file = fileInput.files[0];
+
+ if (!file) {
+ Swal.fire("Error!", "Please select a file to upload.", "error");
+ return;
+ }
+ if (!file.name.toLowerCase().endsWith('.zip')) {
+ Swal.fire("Error!", "Only .zip files are allowed for restore.", "error");
+ return;
+ }
+
+ confirmAction(`restore the system from the selected backup file (${file.name})`, function() {
+ var formData = new FormData();
+ formData.append('file', file);
+
+ var progressBar = document.getElementById('backup_progress_bar');
+ var progressContainer = progressBar.parentElement;
+ var statusDiv = document.getElementById('backup_status');
+
+ progressContainer.style.display = 'block';
+ progressBar.style.width = '0%';
+ progressBar.setAttribute('aria-valuenow', 0);
+ statusDiv.innerText = 'Uploading...';
+ statusDiv.className = 'mt-2';
+
+ $.ajax({
+ url: API_URLS.restore,
+ type: "POST",
+ data: formData,
+ processData: false,
+ contentType: false,
+ xhr: function() {
+ var xhr = new window.XMLHttpRequest();
+ xhr.upload.addEventListener("progress", function(evt) {
+ if (evt.lengthComputable) {
+ var percentComplete = Math.round((evt.loaded / evt.total) * 100);
+ progressBar.style.width = percentComplete + '%';
+ progressBar.setAttribute('aria-valuenow', percentComplete);
+ statusDiv.innerText = `Uploading... ${percentComplete}%`;
+ }
+ }, false);
+ return xhr;
+ },
+ success: function(response) {
+ progressBar.style.width = '100%';
+ progressBar.classList.add('bg-success');
+ statusDiv.innerText = 'Backup restored successfully! Reloading page...';
+ statusDiv.className = 'mt-2 text-success';
+ Swal.fire("Success!", "Backup restored successfully!", "success").then(() => {
+ location.reload();
+ });
+ console.log("Restore Success:", response);
+ },
+ error: function(xhr, status, error) {
+ progressBar.classList.add('bg-danger');
+ let detail = (xhr.responseJSON && xhr.responseJSON.detail) ? xhr.responseJSON.detail : 'Check console for details.';
+ statusDiv.innerText = `Error restoring backup: ${detail}`;
+ statusDiv.className = 'mt-2 text-danger';
+ Swal.fire("Error!", `Failed to restore backup. ${detail}`, "error");
+ console.error("Restore Error:", status, error, xhr.responseText);
+ },
+ complete: function() {
+ fileInput.value = '';
+ }
+ });
+ });
+ }
+
+ function startIPLimit() {
+ confirmAction("start the IP Limit service", function () {
+ sendRequest(
+ API_URLS.startIpLimit,
+ "POST",
+ null,
+ "IP Limit service started successfully!",
+ "#ip_limit_start"
+ );
+ });
+ }
+
+ function stopIPLimit() {
+ confirmAction("stop the IP Limit service", function () {
+ sendRequest(
+ API_URLS.stopIpLimit,
+ "POST",
+ null,
+ "IP Limit service stopped successfully!",
+ "#ip_limit_stop"
+ );
+ });
+ }
+
+ function configIPLimit() {
+ if (!validateForm('ip_limit_config_form')) return;
+ const blockDuration = $("#block_duration").val();
+ const maxIps = $("#max_ips").val();
+ confirmAction("save the IP Limit configuration", function () {
+ sendRequest(
+ API_URLS.configIpLimit,
+ "POST",
+ { block_duration: parseInt(blockDuration), max_ips: parseInt(maxIps) },
+ "IP Limit configuration saved successfully!",
+ "#ip_limit_change_config",
+ false,
+ fetchIpLimitConfig
+ );
+ });
+ }
+
+ function fetchWarpFullStatusAndConfig() {
+ $.ajax({
+ url: API_URLS.statusWarp,
+ type: "GET",
+ success: function (data) {
+ $("#warp_all_traffic").prop('checked', data.all_traffic_via_warp || false);
+ $("#warp_popular_sites").prop('checked', data.popular_sites_via_warp || false);
+ $("#warp_domestic_sites").prop('checked', data.domestic_sites_via_warp || false);
+ $("#warp_block_adult_sites").prop('checked', data.block_adult_content || false);
+
+ $("#warp_initial_controls").hide();
+ $("#warp_active_controls").show();
+ },
+ error: function (xhr, status, error) {
+ let errorMsg = "Failed to fetch WARP configuration.";
+ if (xhr.responseJSON && xhr.responseJSON.detail) {
+ errorMsg = xhr.responseJSON.detail;
+ }
+ console.error("Error fetching WARP config:", errorMsg, xhr.responseText);
+
+ if (xhr.status === 404) {
+ $("#warp_initial_controls").show();
+ $("#warp_active_controls").hide();
+ if ($("#warp_config_form").length > 0) {
+ $("#warp_config_form")[0].reset();
+ }
+ Swal.fire("Info", "WARP service might not be fully configured. Please try reinstalling if issues persist.", "info");
+ } else {
+ if ($("#warp_config_form").length > 0) {
+ $("#warp_config_form")[0].reset();
+ }
+ Swal.fire("Warning", "Could not load current WARP configuration values. Please check manually or re-save.", "warning");
+ }
+ }
+ });
+ }
+
+ function updateGeo(country) {
+ const countryName = country.charAt(0).toUpperCase() + country.slice(1);
+ const buttonId = `#geo_update_${country}`;
+ const url = API_URLS.updateGeoTemplate.replace('COUNTRY_PLACEHOLDER', country);
+
+ confirmAction(`update the Geo files for ${countryName}`, function () {
+ sendRequest(
+ url,
+ "GET",
+ null,
+ `Geo files for ${countryName} updated successfully!`,
+ buttonId,
+ false,
+ null
+ );
+ });
+ }
+
+ $("#warp_start_btn").on("click", function() {
+ confirmAction("install and start WARP", function () {
+ sendRequest(
+ API_URLS.installWarp,
+ "POST",
+ null,
+ "WARP installation request sent. The page will reload.",
+ "#warp_start_btn",
+ true
+ );
+ });
+ });
+
+ $("#warp_stop_btn").on("click", function() {
+ confirmAction("stop and uninstall WARP", function () {
+ sendRequest(
+ API_URLS.uninstallWarp,
+ "DELETE",
+ null,
+ "WARP uninstallation request sent. The page will reload.",
+ "#warp_stop_btn",
+ true
+ );
+ });
+ });
+
+ $("#warp_save_config_btn").on("click", function() {
+ const configData = {
+ all: $("#warp_all_traffic").is(":checked"),
+ popular_sites: $("#warp_popular_sites").is(":checked"),
+ domestic_sites: $("#warp_domestic_sites").is(":checked"),
+ block_adult_sites: $("#warp_block_adult_sites").is(":checked")
+ };
+ confirmAction("save WARP configuration", function () {
+ sendRequest(
+ API_URLS.configureWarp,
+ "POST",
+ configData,
+ "WARP configuration saved successfully!",
+ "#warp_save_config_btn",
+ false,
+ fetchWarpFullStatusAndConfig
+ );
+ });
+ });
+
+ $("#telegram_start").on("click", startTelegram);
+ $("#telegram_stop").on("click", stopTelegram);
+ $("#telegram_save_interval").on("click", saveTelegramInterval);
+ $("#normal_start").on("click", startNormal);
+ $("#normal_stop").on("click", stopNormal);
+ $("#normal_subpath_save_btn").on("click", editNormalSubPath);
+ $("#port_change").on("click", changePort);
+ $("#sni_change").on("click", changeSNI);
+ $("#ip_change").on("click", saveIP);
+ $("#download_backup").on("click", downloadBackup);
+ $("#upload_backup").on("click", uploadBackup);
+ $("#ip_limit_start").on("click", startIPLimit);
+ $("#ip_limit_stop").on("click", stopIPLimit);
+ $("#ip_limit_change_config").on("click", configIPLimit);
+ $("#decoy_setup").on("click", setupDecoy);
+ $("#decoy_stop").on("click", stopDecoy);
+ $("#obfs_enable_btn").on("click", enableObfs);
+ $("#obfs_disable_btn").on("click", disableObfs);
+ $("#add_node_btn").on("click", addNode);
+ $("#nodes_table").on("click", ".delete-node-btn", function() {
+ const nodeName = $(this).data("name");
+ deleteNode(nodeName);
+ });
+ $("#add_extra_config_btn").on("click", addExtraConfig);
+ $("#extra_configs_table").on("click", ".delete-extra-config-btn", function() {
+ const configName = $(this).data("name");
+ deleteExtraConfig(configName);
+ });
+ $("#geo_update_iran").on("click", function() { updateGeo('iran'); });
+ $("#geo_update_china").on("click", function() { updateGeo('china'); });
+ $("#geo_update_russia").on("click", function() { updateGeo('russia'); });
+
+ $('#normal_domain, #sni_domain, #decoy_domain').on('input', function () {
+ if (isValidDomain($(this).val())) {
+ $(this).removeClass('is-invalid');
+ } else if ($(this).val().trim() !== "") {
+ $(this).addClass('is-invalid');
+ } else {
+ $(this).removeClass('is-invalid');
+ }
+ });
+
+ $('#normal_port, #hysteria_port').on('input', function () {
+ if (isValidPort($(this).val())) {
+ $(this).removeClass('is-invalid');
+ } else if ($(this).val().trim() !== "") {
+ $(this).addClass('is-invalid');
+ } else {
+ $(this).removeClass('is-invalid');
+ }
+ });
+
+ $('#normal_subpath_input').on('input', function () {
+ if (isValidSubPath($(this).val())) {
+ $(this).removeClass('is-invalid');
+ } else if ($(this).val().trim() !== "") {
+ $(this).addClass('is-invalid');
+ } else {
+ $(this).removeClass('is-invalid');
+ }
+ });
+
+ $('#ipv4, #ipv6, #node_ip').on('input', function () {
+ const isLocalIpField = $(this).attr('id') === 'ipv4' || $(this).attr('id') === 'ipv6';
+ if (isLocalIpField && $(this).val().trim() === '') {
+ $(this).removeClass('is-invalid');
+ } else if (isValidIPorDomain($(this).val())) {
+ $(this).removeClass('is-invalid');
+ } else {
+ $(this).addClass('is-invalid');
+ }
+ });
+
+ $('#node_name, #extra_config_name').on('input', function() {
+ if ($(this).val().trim() !== "") {
+ $(this).removeClass('is-invalid');
+ } else {
+ $(this).addClass('is-invalid');
+ }
+ });
+
+ $('#extra_config_uri').on('input', function () {
+ if (isValidURI($(this).val())) {
+ $(this).removeClass('is-invalid');
+ } else if ($(this).val().trim() !== "") {
+ $(this).addClass('is-invalid');
+ }
+ });
+
+ $('#telegram_api_token, #telegram_admin_id').on('input', function () {
+ if ($(this).val().trim() !== "") {
+ $(this).removeClass('is-invalid');
+ } else {
+ $(this).addClass('is-invalid');
+ }
+ });
+ $('#block_duration, #max_ips, #telegram_backup_interval').on('input', function () {
+ if ($(this).attr('id') === 'telegram_backup_interval' && $(this).val().trim() === '') {
+ $(this).removeClass('is-invalid');
+ return;
+ }
+ if (isValidPositiveNumber($(this).val())) {
+ $(this).removeClass('is-invalid');
+ } else if ($(this).val().trim() !== "") {
+ $(this).addClass('is-invalid');
+ } else {
+ $(this).addClass('is-invalid');
+ }
+ });
+
+ $('#decoy_path').on('input', function () {
+ if (isValidPath($(this).val())) {
+ $(this).removeClass('is-invalid');
+ } else if ($(this).val().trim() !== "") {
+ $(this).addClass('is-invalid');
+ } else {
+ $(this).addClass('is-invalid');
+ }
+ });
+});
\ No newline at end of file
diff --git a/core/scripts/webpanel/assets/js/users.js b/core/scripts/webpanel/assets/js/users.js
new file mode 100644
index 0000000..a8ec42e
--- /dev/null
+++ b/core/scripts/webpanel/assets/js/users.js
@@ -0,0 +1,73 @@
+function updateServerInfo() {
+ const serverStatusUrl = document.querySelector('.content').dataset.serverStatusUrl;
+ fetch(serverStatusUrl)
+ .then(response => response.json())
+ .then(data => {
+ document.getElementById('cpu-usage').textContent = data.cpu_usage;
+ document.getElementById('ram-usage').textContent = `${data.ram_usage} / ${data.total_ram}`;
+ document.getElementById('online-users').textContent = data.online_users;
+ document.getElementById('uptime').textContent = data.uptime;
+
+ document.getElementById('server-ipv4').textContent = `IPv4: ${data.server_ipv4 || 'N/A'}`;
+ document.getElementById('server-ipv6').textContent = `IPv6: ${data.server_ipv6 || 'N/A'}`;
+
+ document.getElementById('download-speed').textContent = `🔽 Download: ${data.download_speed}`;
+ document.getElementById('upload-speed').textContent = `🔼 Upload: ${data.upload_speed}`;
+ document.getElementById('tcp-connections').textContent = `TCP: ${data.tcp_connections}`;
+ document.getElementById('udp-connections').textContent = `UDP: ${data.udp_connections}`;
+
+ document.getElementById('reboot-uploaded-traffic').textContent = data.reboot_uploaded_traffic;
+ document.getElementById('reboot-downloaded-traffic').textContent = data.reboot_downloaded_traffic;
+ document.getElementById('reboot-total-traffic').textContent = data.reboot_total_traffic;
+
+ document.getElementById('user-uploaded-traffic').textContent = data.user_uploaded_traffic;
+ document.getElementById('user-downloaded-traffic').textContent = data.user_downloaded_traffic;
+ document.getElementById('user-total-traffic').textContent = data.user_total_traffic;
+ })
+ .catch(error => console.error('Error fetching server info:', error));
+}
+
+function updateServiceStatuses() {
+ const servicesStatusUrl = document.querySelector('.content').dataset.servicesStatusUrl;
+ fetch(servicesStatusUrl)
+ .then(response => response.json())
+ .then(data => {
+ updateServiceBox('hysteria2', data.hysteria_server);
+ updateServiceBox('telegrambot', data.hysteria_telegram_bot);
+ updateServiceBox('iplimit', data.hysteria_iplimit);
+ updateServiceBox('normalsub', data.hysteria_normal_sub);
+ })
+ .catch(error => console.error('Error fetching service statuses:', error));
+}
+
+function updateServiceBox(serviceName, status) {
+ const statusElement = document.getElementById(serviceName + '-status');
+ const statusBox = document.getElementById(serviceName + '-status-box');
+
+ if (status === true) {
+ statusElement.textContent = 'Active';
+ statusBox.classList.remove('bg-danger');
+ statusBox.classList.add('bg-success');
+ } else {
+ statusElement.textContent = 'Inactive';
+ statusBox.classList.remove('bg-success');
+ statusBox.classList.add('bg-danger');
+ }
+}
+
+document.addEventListener('DOMContentLoaded', function () {
+ updateServerInfo();
+ updateServiceStatuses();
+ setInterval(updateServerInfo, 2000);
+ setInterval(updateServiceStatuses, 10000);
+
+ const toggleIpBtn = document.getElementById('toggle-ip-visibility');
+ const ipAddressesDiv = document.getElementById('ip-addresses');
+ toggleIpBtn.addEventListener('click', function(e) {
+ e.preventDefault();
+ const isBlurred = ipAddressesDiv.style.filter === 'blur(5px)';
+ ipAddressesDiv.style.filter = isBlurred ? 'none' : 'blur(5px)';
+ toggleIpBtn.querySelector('i').classList.toggle('fa-eye');
+ toggleIpBtn.querySelector('i').classList.toggle('fa-eye-slash');
+ });
+});
\ No newline at end of file