Build sdxpinn-quecmanger ipk
-Removed no longer needed files -Created ipk for quecmanager -Thank you @dr-dolomite for your hard work on this! Co-Authored-By: Russel Yasol <73575327+dr-dolomite@users.noreply.github.com>
This commit is contained in:
347
ipk-source/sdxpinn-quecmanager/root/www/js/about/fetch-data.js
Normal file
347
ipk-source/sdxpinn-quecmanager/root/www/js/about/fetch-data.js
Normal file
@@ -0,0 +1,347 @@
|
||||
// State variables to track current values
|
||||
let currentIMEI = "";
|
||||
let updatedIMEI = "";
|
||||
|
||||
// Constants
|
||||
const REBOOT_COUNTDOWN_TIME = 80;
|
||||
const MESSAGES = {
|
||||
DEFAULT_REBOOT: "Do not do any action while the modem is rebooting.",
|
||||
IMEI_REBOOT:
|
||||
"IMEI change requires a reboot.\nDo not perform any actions while the modem is rebooting.",
|
||||
INVALID_IMEI: "IMEI should be 15 digits and should only contain numbers.",
|
||||
NO_CHANGES: "No changes detected in the IMEI field.",
|
||||
ERROR_SAVING: "Error saving settings. Please try again.",
|
||||
};
|
||||
|
||||
const DATA_MAP = {
|
||||
CGMI: {
|
||||
parse: (response) => response.split("\n")[1].trim(),
|
||||
elementId: "manufacturer",
|
||||
},
|
||||
CGMM: {
|
||||
parse: (response) => response.split("\n")[1].trim(),
|
||||
elementId: "model",
|
||||
},
|
||||
CGMR: {
|
||||
parse: (response) => response.split("\n")[1].trim(),
|
||||
elementId: "firmwareVersion",
|
||||
},
|
||||
CNUM: {
|
||||
parse: (response) =>
|
||||
response
|
||||
.split("\n")[1]
|
||||
.split(":")[1]
|
||||
.split(",")[1]
|
||||
.replace(/"/g, "")
|
||||
.trim(),
|
||||
elementId: "phoneNumber",
|
||||
},
|
||||
CIMI: {
|
||||
parse: (response) => response.split("\n")[1].trim(),
|
||||
elementId: "imsi",
|
||||
},
|
||||
ICCID: {
|
||||
parse: (response) => response.split("\n")[1].split(":")[1].trim(),
|
||||
elementId: "iccid",
|
||||
},
|
||||
CGSN: {
|
||||
parse: (response) => response.split("\n")[1].trim(),
|
||||
elementId: "imei",
|
||||
special: true,
|
||||
},
|
||||
LANIP: {
|
||||
parse: (response) =>
|
||||
response.split("\n")[1].split(":")[1].split(",")[3].trim(),
|
||||
elementId: "lanIP",
|
||||
},
|
||||
WWAN: {
|
||||
parse: (response) => ({
|
||||
IPv4: response
|
||||
.split("\n")[1]
|
||||
.split(":")[1]
|
||||
.split(",")[4]
|
||||
.replace(/"/g, "")
|
||||
.trim(),
|
||||
IPv6: response.split("\n")[2].split(",")[4].replace(/"/g, "").trim(),
|
||||
}),
|
||||
elementIds: ["IPv4", "IPv6"],
|
||||
},
|
||||
};
|
||||
|
||||
// DOM Element Selectors
|
||||
const selectors = {
|
||||
modal: "#reboot-modal",
|
||||
countdown: "#countdown",
|
||||
loadingContent: "#loading-content",
|
||||
modalButtons: "#modal-buttons",
|
||||
modalMessage: "#modal-message",
|
||||
imeiInput: "#imeiInput",
|
||||
changeButton: "#changeButton",
|
||||
powerButton: ".reboot-modal",
|
||||
alertButtons: ".delete",
|
||||
modalBackground: ".modal-background",
|
||||
cancelButton: ".cancel",
|
||||
rebootButton: "#rebootModem",
|
||||
};
|
||||
|
||||
// Utility Functions
|
||||
function getElement(selector) {
|
||||
return document.querySelector(selector);
|
||||
}
|
||||
|
||||
function validateIMEI(imei) {
|
||||
return imei.length === 15 && !isNaN(imei);
|
||||
}
|
||||
|
||||
function updateElementDisplay(element, display) {
|
||||
if (element) element.style.display = display;
|
||||
}
|
||||
|
||||
// IMEI Management Functions
|
||||
function haveIMEIChanged() {
|
||||
return currentIMEI !== updatedIMEI;
|
||||
}
|
||||
|
||||
function resetIMEIInput() {
|
||||
const imeiInput = getElement(selectors.imeiInput);
|
||||
if (imeiInput) {
|
||||
imeiInput.value = currentIMEI;
|
||||
updatedIMEI = currentIMEI;
|
||||
}
|
||||
}
|
||||
|
||||
// AT Command Functions
|
||||
async function sendATCommand(command) {
|
||||
try {
|
||||
const response = await fetch("/cgi-bin/atinout_handler.sh", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
},
|
||||
body: "command=" + encodeURIComponent(command),
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error("Error sending AT command:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Modal Management Functions
|
||||
function handleRebootCountdown() {
|
||||
const countdownElement = getElement(selectors.countdown);
|
||||
const loadingContent = getElement(selectors.loadingContent);
|
||||
const modalButtons = getElement(selectors.modalButtons);
|
||||
const modalMessage = getElement(selectors.modalMessage);
|
||||
|
||||
updateElementDisplay(modalMessage, "none");
|
||||
updateElementDisplay(modalButtons, "none");
|
||||
updateElementDisplay(loadingContent, "block");
|
||||
|
||||
let timeLeft = REBOOT_COUNTDOWN_TIME;
|
||||
|
||||
const countdownInterval = setInterval(() => {
|
||||
timeLeft--;
|
||||
if (countdownElement) countdownElement.textContent = timeLeft;
|
||||
|
||||
if (timeLeft <= 0) {
|
||||
clearInterval(countdownInterval);
|
||||
window.location.reload();
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
function showRebootModal(isIMEIChange = false) {
|
||||
const modal = getElement(selectors.modal);
|
||||
const loadingContent = getElement(selectors.loadingContent);
|
||||
const modalButtons = getElement(selectors.modalButtons);
|
||||
const modalMessage = getElement(selectors.modalMessage);
|
||||
|
||||
if (!modal) return;
|
||||
|
||||
modal.classList.add("is-active");
|
||||
|
||||
updateElementDisplay(loadingContent, "none");
|
||||
updateElementDisplay(modalButtons, "block");
|
||||
updateElementDisplay(modalMessage, "block");
|
||||
|
||||
if (modalMessage) {
|
||||
modalMessage.textContent = isIMEIChange
|
||||
? MESSAGES.IMEI_REBOOT
|
||||
: MESSAGES.DEFAULT_REBOOT;
|
||||
modalMessage.style.whiteSpace = "pre-line";
|
||||
}
|
||||
|
||||
setupModalEventListeners(modal, isIMEIChange);
|
||||
}
|
||||
|
||||
function setupModalEventListeners(modal, isIMEIChange) {
|
||||
const rebootButton = getElement(selectors.rebootButton);
|
||||
const cancelButton = modal.querySelector(selectors.cancelButton);
|
||||
const modalBackground = modal.querySelector(selectors.modalBackground);
|
||||
|
||||
if (rebootButton) {
|
||||
rebootButton.onclick = async () => {
|
||||
handleRebootCountdown();
|
||||
if (isIMEIChange) {
|
||||
try {
|
||||
await sendATCommand("AT+QPOWD=1");
|
||||
} catch (error) {
|
||||
console.error("Error sending reboot command:", error);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const closeModal = () => {
|
||||
modal.classList.remove("is-active");
|
||||
if (isIMEIChange) {
|
||||
resetIMEIInput();
|
||||
}
|
||||
};
|
||||
|
||||
if (cancelButton) cancelButton.onclick = closeModal;
|
||||
if (modalBackground) modalBackground.onclick = closeModal;
|
||||
}
|
||||
|
||||
// IMEI Settings Management
|
||||
async function saveIMEISetting() {
|
||||
if (!haveIMEIChanged()) {
|
||||
alert(MESSAGES.NO_CHANGES);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!validateIMEI(updatedIMEI)) {
|
||||
alert(MESSAGES.INVALID_IMEI);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const atCommand = `AT+EGMR=1,7,"${updatedIMEI}"`;
|
||||
console.log("Sending AT command:", atCommand);
|
||||
|
||||
const inputs = document.querySelectorAll("input, select");
|
||||
inputs.forEach((input) => (input.disabled = true));
|
||||
|
||||
const response = await sendATCommand(atCommand);
|
||||
console.log("AT command response:", response);
|
||||
|
||||
showRebootModal(true);
|
||||
} catch (error) {
|
||||
console.error("Error saving settings:", error);
|
||||
alert(MESSAGES.ERROR_SAVING);
|
||||
resetIMEIInput();
|
||||
|
||||
const inputs = document.querySelectorAll("input, select");
|
||||
inputs.forEach((input) => (input.disabled = false));
|
||||
}
|
||||
}
|
||||
|
||||
// Data Parsing Functions
|
||||
function parseDeviceData(response, key) {
|
||||
const dataMap = {
|
||||
CGMI: (response) => response.split("\n")[1].trim(),
|
||||
CGMM: (response) => response.split("\n")[1].trim(),
|
||||
CGMR: (response) => response.split("\n")[1].trim(),
|
||||
CNUM: (response) =>
|
||||
response
|
||||
.split("\n")[1]
|
||||
.split(":")[1]
|
||||
.split(",")[1]
|
||||
.replace(/"/g, "")
|
||||
.trim(),
|
||||
CIMI: (response) => response.split("\n")[1].trim(),
|
||||
ICCID: (response) => response.split("\n")[1].split(":")[1].trim(),
|
||||
CGSN: (response) => response.split("\n")[1].trim(),
|
||||
LANIP: (response) =>
|
||||
response.split("\n")[1].split(":")[1].split(",")[3].trim(),
|
||||
WWAN: (response) => ({
|
||||
IPv4: response
|
||||
.split("\n")[1]
|
||||
.split(":")[1]
|
||||
.split(",")[4]
|
||||
.replace(/"/g, "")
|
||||
.trim(),
|
||||
IPv6: response.split("\n")[2].split(",")[4].replace(/"/g, "").trim(),
|
||||
}),
|
||||
};
|
||||
|
||||
return dataMap[key]?.(response);
|
||||
}
|
||||
|
||||
// Data Fetching and Display
|
||||
// Data Parsing and Update Functions
|
||||
function updateDeviceInfo(key, value) {
|
||||
const mapping = DATA_MAP[key];
|
||||
if (!mapping) return;
|
||||
|
||||
if (mapping.elementIds) {
|
||||
// Handle WWAN case with multiple values
|
||||
mapping.elementIds.forEach((id) => {
|
||||
const element = document.getElementById(id);
|
||||
if (element) element.textContent = value[id];
|
||||
});
|
||||
} else {
|
||||
const element = document.getElementById(mapping.elementId);
|
||||
if (element) element.textContent = value;
|
||||
|
||||
// Special handling for IMEI
|
||||
if (mapping.special) {
|
||||
currentIMEI = value;
|
||||
updatedIMEI = value;
|
||||
const imeiInput = getElement(selectors.imeiInput);
|
||||
if (imeiInput) {
|
||||
imeiInput.value = value;
|
||||
imeiInput.addEventListener("input", () => {
|
||||
updatedIMEI = imeiInput.value;
|
||||
console.log("Updated IMEI:", updatedIMEI);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Data Fetching
|
||||
async function fetchAboutData() {
|
||||
try {
|
||||
const response = await fetch("/cgi-bin/about/fetch-about.sh");
|
||||
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
|
||||
|
||||
const data = await response.json();
|
||||
console.log("Full response:", data);
|
||||
|
||||
data.forEach((item) => {
|
||||
Object.keys(DATA_MAP).forEach((key) => {
|
||||
if (item.response.includes(key)) {
|
||||
const value = DATA_MAP[key].parse(item.response);
|
||||
updateDeviceInfo(key, value);
|
||||
}
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error fetching about data:", error);
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize when DOM is loaded
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
fetchAboutData();
|
||||
|
||||
const changeButton = getElement(selectors.changeButton);
|
||||
if (changeButton) {
|
||||
changeButton.addEventListener("click", saveIMEISetting);
|
||||
}
|
||||
|
||||
const powerButton = getElement(selectors.powerButton);
|
||||
if (powerButton) {
|
||||
powerButton.addEventListener("click", () => showRebootModal(false));
|
||||
}
|
||||
|
||||
const alertButtons = document.querySelectorAll(selectors.alertButtons);
|
||||
alertButtons.forEach((button) => {
|
||||
button.addEventListener("click", fetchAboutData);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,135 @@
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
const form = document.getElementById("commandForm");
|
||||
const output = document.getElementById("output");
|
||||
const commandInput = document.getElementById("command");
|
||||
const sendButton = document.getElementById("sendButton");
|
||||
const commandHistory = document.getElementById("commandHistory");
|
||||
const noHistory = document.getElementById("noHistory");
|
||||
const clearHistoryButton = document.getElementById("clearHistory");
|
||||
const cooldownTimer = document.getElementById("cooldownTimer");
|
||||
|
||||
const COOLDOWN_DURATION = 1000; // 1 second cooldown
|
||||
let isLoading = false;
|
||||
let cooldownActive = false;
|
||||
|
||||
function setLoading(loading) {
|
||||
isLoading = loading;
|
||||
sendButton.classList.toggle("is-loading", loading);
|
||||
form.classList.toggle("loading", loading);
|
||||
}
|
||||
|
||||
function setCooldown() {
|
||||
cooldownActive = true;
|
||||
sendButton.classList.add("cooldown");
|
||||
let timeLeft = COOLDOWN_DURATION;
|
||||
|
||||
function updateTimer() {
|
||||
timeLeft -= 100;
|
||||
if (timeLeft <= 0) {
|
||||
cooldownActive = false;
|
||||
sendButton.classList.remove("cooldown");
|
||||
cooldownTimer.textContent = "";
|
||||
return;
|
||||
}
|
||||
cooldownTimer.textContent = `${(timeLeft / 1000).toFixed(1)}s`;
|
||||
setTimeout(updateTimer, 100);
|
||||
}
|
||||
|
||||
updateTimer();
|
||||
}
|
||||
|
||||
function updateHistoryVisibility() {
|
||||
const hasHistoryItems =
|
||||
commandHistory.querySelectorAll(".history-item").length > 0;
|
||||
noHistory.style.display = hasHistoryItems ? "none" : "block";
|
||||
clearHistoryButton.style.display = hasHistoryItems ? "block" : "none";
|
||||
}
|
||||
|
||||
function addToHistory(command, response) {
|
||||
const historyItem = document.createElement("div");
|
||||
historyItem.className = "box mb-2 history-item";
|
||||
historyItem.innerHTML = `
|
||||
<button class="delete delete-history" aria-label="delete"></button>
|
||||
<strong class="command-text">${command}</strong>
|
||||
<pre style="margin-top: 0.5rem; font-size: 0.85em; white-space: pre-wrap;">${response}</pre>
|
||||
`;
|
||||
|
||||
historyItem
|
||||
.querySelector(".command-text")
|
||||
.addEventListener("click", () => {
|
||||
commandInput.value = command;
|
||||
commandInput.focus();
|
||||
});
|
||||
|
||||
historyItem
|
||||
.querySelector(".delete-history")
|
||||
.addEventListener("click", (e) => {
|
||||
e.stopPropagation();
|
||||
historyItem.classList.add("fade-out");
|
||||
setTimeout(() => {
|
||||
historyItem.remove();
|
||||
updateHistoryVisibility();
|
||||
}, 300);
|
||||
});
|
||||
|
||||
commandHistory.insertBefore(historyItem, commandHistory.firstChild);
|
||||
updateHistoryVisibility();
|
||||
}
|
||||
|
||||
clearHistoryButton.addEventListener("click", () => {
|
||||
const historyItems = commandHistory.querySelectorAll(".history-item");
|
||||
historyItems.forEach((item) => {
|
||||
item.classList.add("fade-out");
|
||||
});
|
||||
setTimeout(() => {
|
||||
commandHistory.innerHTML = "";
|
||||
commandHistory.appendChild(noHistory);
|
||||
updateHistoryVisibility();
|
||||
}, 300);
|
||||
});
|
||||
|
||||
async function sendCommand(command) {
|
||||
try {
|
||||
setLoading(true);
|
||||
const response = await fetch("/cgi-bin/atinout_handler.sh", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
},
|
||||
body: `command=${encodeURIComponent(command)}`,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
output.value = data.output || "No response received";
|
||||
addToHistory(command, data.output || "No response received");
|
||||
setCooldown();
|
||||
} catch (error) {
|
||||
const errorMessage = `Error: ${error.message}\n\nTroubleshooting steps:\n1. Check if the device is connected\n2. Verify AT port settings\n3. Ensure atinout utility is installed`;
|
||||
output.value = errorMessage;
|
||||
addToHistory(command, errorMessage);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
form.addEventListener("submit", async function (e) {
|
||||
e.preventDefault();
|
||||
if (isLoading || cooldownActive) return;
|
||||
|
||||
const command = commandInput.value.trim();
|
||||
if (!command) {
|
||||
output.value = "Please enter a command";
|
||||
return;
|
||||
}
|
||||
|
||||
await sendCommand(command);
|
||||
commandInput.value = "";
|
||||
});
|
||||
|
||||
// Initialize visibility
|
||||
updateHistoryVisibility();
|
||||
});
|
||||
@@ -0,0 +1,377 @@
|
||||
// API Module - Handles all server communications
|
||||
const api = {
|
||||
async fetchCurrentSettings() {
|
||||
try {
|
||||
const response = await fetch("/cgi-bin/advance/advanced_settings.sh");
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
const data = await response.json();
|
||||
console.log("Current settings:", data);
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error("Error fetching settings:", error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
async fetchConnectedDevices() {
|
||||
try {
|
||||
const response = await fetch("/cgi-bin/advance/fetch_macs.sh");
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
const data = await response.json();
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error("Error fetching devices:", error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
async sendATCommand(command) {
|
||||
try {
|
||||
const response = await fetch("/cgi-bin/atinout_handler.sh", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
body: "command=" + encodeURIComponent(command)
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
const data = await response.json();
|
||||
console.log("AT command response:", data);
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error("Error sending AT command:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// UI Manager Module - Handles all DOM interactions and UI updates
|
||||
const uiManager = {
|
||||
elements: {
|
||||
ipPassthrough: () => document.getElementById("ip-passthrough-mode"),
|
||||
dnsProxy: () => document.getElementById("dns-proxy-mode"),
|
||||
usbModem: () => document.getElementById("usb-modem-protocol"),
|
||||
connectedDevices: () => document.getElementById("connected-devices"),
|
||||
loadingContent: () => document.getElementById("loading-content"),
|
||||
modalButtons: () => document.getElementById("modal-buttons"),
|
||||
countdown: () => document.getElementById("countdown"),
|
||||
rebootModal: () => document.getElementById("reboot-modal"),
|
||||
advancedSettingsIcons: () => document.querySelectorAll(".advanced-settings i")
|
||||
},
|
||||
|
||||
showLoadingSpinners() {
|
||||
this.elements.advancedSettingsIcons().forEach(icon => {
|
||||
icon.classList.add("fa-spinner", "fa-spin");
|
||||
});
|
||||
},
|
||||
|
||||
hideLoadingSpinners() {
|
||||
this.elements.advancedSettingsIcons().forEach(icon => {
|
||||
icon.classList.remove("fa-spinner", "fa-spin");
|
||||
});
|
||||
},
|
||||
|
||||
updatePassthroughModeState(isEnabled) {
|
||||
const select = this.elements.ipPassthrough();
|
||||
if (!select) return;
|
||||
|
||||
const helpText = select.parentElement.querySelector(".help");
|
||||
|
||||
if (isEnabled) {
|
||||
select.removeAttribute("disabled");
|
||||
select.classList.remove("is-warning");
|
||||
if (helpText) {
|
||||
helpText.textContent = "Select a passthrough mode to apply.";
|
||||
helpText.classList.remove("is-warning");
|
||||
helpText.classList.add("is-info");
|
||||
}
|
||||
} else {
|
||||
select.setAttribute("disabled", "disabled");
|
||||
select.classList.add("is-warning");
|
||||
select.value = "Select IP Passthrough Mode";
|
||||
if (helpText) {
|
||||
helpText.textContent = "Please select a device first.";
|
||||
helpText.classList.remove("is-info");
|
||||
helpText.classList.add("is-warning");
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
populateConnectedDevices(devices) {
|
||||
const select = this.elements.connectedDevices();
|
||||
if (!select) {
|
||||
console.error("Connected devices select element not found");
|
||||
return;
|
||||
}
|
||||
|
||||
// Clear existing options except the first one
|
||||
while (select.options.length > 1) {
|
||||
select.remove(1);
|
||||
}
|
||||
|
||||
// Add new options
|
||||
devices.forEach(device => {
|
||||
const option = document.createElement("option");
|
||||
option.value = device.mac;
|
||||
option.textContent = `${device.hostname} - ${device.mac}`;
|
||||
select.appendChild(option);
|
||||
});
|
||||
},
|
||||
|
||||
showModal() {
|
||||
const modal = this.elements.rebootModal();
|
||||
if (modal) {
|
||||
modal.classList.add("is-active");
|
||||
}
|
||||
},
|
||||
|
||||
showLoadingContent() {
|
||||
this.elements.loadingContent().style.display = "flex";
|
||||
this.elements.modalButtons().style.display = "none";
|
||||
this.showModal();
|
||||
},
|
||||
|
||||
startCountdown(duration) {
|
||||
const countdownElement = this.elements.countdown();
|
||||
let countdown = duration;
|
||||
|
||||
const interval = setInterval(() => {
|
||||
countdown--;
|
||||
countdownElement.textContent = countdown;
|
||||
|
||||
if (countdown <= 0) {
|
||||
clearInterval(interval);
|
||||
location.reload();
|
||||
}
|
||||
}, 1000);
|
||||
},
|
||||
|
||||
setElementLoading(element, isLoading) {
|
||||
if (isLoading) {
|
||||
element.disabled = true;
|
||||
element.classList.add('is-loading');
|
||||
} else {
|
||||
element.disabled = false;
|
||||
element.classList.remove('is-loading');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Settings Manager Module - Handles settings logic and updates
|
||||
const settingsManager = {
|
||||
async updateSettings(data) {
|
||||
const elements = {
|
||||
ipPassthrough: uiManager.elements.ipPassthrough(),
|
||||
dnsProxy: uiManager.elements.dnsProxy(),
|
||||
usbModem: uiManager.elements.usbModem()
|
||||
};
|
||||
|
||||
// Validate required elements
|
||||
const missingElements = Object.entries(elements)
|
||||
.filter(([, element]) => !element)
|
||||
.map(([key]) => key);
|
||||
|
||||
if (missingElements.length > 0) {
|
||||
console.error("Missing DOM elements:", missingElements);
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
uiManager.updatePassthroughModeState(false);
|
||||
|
||||
// Update IP Passthrough Mode
|
||||
const mpdnRuleLine = data[0].response.split("\n")[1];
|
||||
if (mpdnRuleLine) {
|
||||
const mpdnRule = mpdnRuleLine.split(":")[1].trim();
|
||||
const passthroughMode = this.getPassthroughModeValue(mpdnRule);
|
||||
elements.ipPassthrough.value = passthroughMode;
|
||||
elements.ipPassthrough.setAttribute("data-current-mode", passthroughMode);
|
||||
}
|
||||
|
||||
// Update DNS Proxy
|
||||
const dnsProxyLine = data[1].response.split("\n")[1].split(":")[1].split(",")[1].trim();
|
||||
const dnsProxyMode = dnsProxyLine === '"disable"' ? "Disabled" : "Enabled";
|
||||
elements.dnsProxy.value = dnsProxyMode;
|
||||
elements.dnsProxy.setAttribute("data-current-mode", dnsProxyMode);
|
||||
|
||||
// Update USB Modem Protocol
|
||||
const usbModemProtocolLine = data[2].response.split("\n")[1].split(":")[1].split(",")[1].trim();
|
||||
const usbModemMode = this.getUsbModemProtocolValue(usbModemProtocolLine);
|
||||
elements.usbModem.value = usbModemMode;
|
||||
elements.usbModem.setAttribute("data-current-protocol", usbModemMode);
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error("Error updating settings:", error);
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
getPassthroughModeValue(mpdnRule) {
|
||||
const modes = {
|
||||
'"MPDN_rule",0,0,0,0,0': "Disabled",
|
||||
'"MPDN_rule",0,1,0,1,1': "ETH Only",
|
||||
'"MPDN_rule",0,1,0,3,1': "USB Only"
|
||||
};
|
||||
return modes[mpdnRule] || "Select IP Passthrough Mode";
|
||||
},
|
||||
|
||||
getUsbModemProtocolValue(protocol) {
|
||||
const protocols = {
|
||||
"0": "RMNET",
|
||||
"1": "ECM (Recommended)",
|
||||
"2": "MBIM",
|
||||
"3": "RNDIS"
|
||||
};
|
||||
return protocols[protocol] || "Select USB Modem Protocol";
|
||||
}
|
||||
};
|
||||
|
||||
// Event Handlers Module - Handles all event listeners
|
||||
const eventHandlers = {
|
||||
async handleDnsProxyChange(e) {
|
||||
const element = e.target;
|
||||
const selectedMode = element.value;
|
||||
const currentMode = element.getAttribute("data-current-mode");
|
||||
|
||||
if (selectedMode !== currentMode) {
|
||||
const command = selectedMode === "Enabled"
|
||||
? 'AT+QMAP="DHCPV4DNS","enable"'
|
||||
: 'AT+QMAP="DHCPV4DNS","disable"';
|
||||
|
||||
uiManager.setElementLoading(element, true);
|
||||
|
||||
try {
|
||||
const response = await api.sendATCommand(command);
|
||||
if (response.output.includes("OK")) {
|
||||
element.setAttribute("data-current-mode", selectedMode);
|
||||
// uiManager.showSuccessMessage("DNS Proxy setting updated successfully");
|
||||
} else {
|
||||
element.value = currentMode;
|
||||
// uiManager.showErrorMessage("Failed to update DNS Proxy setting");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error sending AT command:", error);
|
||||
element.value = currentMode;
|
||||
// uiManager.showErrorMessage("Error updating DNS Proxy setting");
|
||||
} finally {
|
||||
// uiManager.setElementLoading(element, false);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
async handleIpPassthroughChange(e) {
|
||||
const element = e.target;
|
||||
const selectedMode = element.value;
|
||||
const currentMode = element.getAttribute("data-current-mode");
|
||||
const selectedDeviceMAC = uiManager.elements.connectedDevices().value;
|
||||
|
||||
if (selectedMode !== currentMode) {
|
||||
const commands = {
|
||||
"Disabled": 'AT+QMAP="MPDN_rule",0;+QPOWD=1',
|
||||
"ETH Only": `AT+QMAP="MPDN_rule",0,1,0,1,1,"${selectedDeviceMAC}";+QPOWD=1`,
|
||||
"USB Only": `AT+QMAP="MPDN_rule",0,1,0,3,1,"${selectedDeviceMAC}";+QPOWD=1`
|
||||
};
|
||||
|
||||
const command = commands[selectedMode];
|
||||
if (command) {
|
||||
uiManager.showLoadingContent();
|
||||
uiManager.startCountdown(90);
|
||||
try {
|
||||
await api.sendATCommand(command);
|
||||
} catch (error) {
|
||||
uiManager.showErrorMessage("Error updating IP Passthrough mode");
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
async handleUsbModemProtocolChange(e) {
|
||||
const element = e.target;
|
||||
const selectedProtocol = element.value;
|
||||
const currentProtocol = element.getAttribute("data-current-protocol");
|
||||
|
||||
if (selectedProtocol !== currentProtocol) {
|
||||
const commands = {
|
||||
"RMNET": 'AT+QCFG="usbnet",0;+CFUN=1,1',
|
||||
"ECM (Recommended)": 'AT+QCFG="usbnet",1;+CFUN=1,1',
|
||||
"MBIM": 'AT+QCFG="usbnet",2;+CFUN=1,1',
|
||||
"RNDIS": 'AT+QCFG="usbnet",3;+CFUN=1,1'
|
||||
};
|
||||
|
||||
const command = commands[selectedProtocol];
|
||||
if (command) {
|
||||
uiManager.showLoadingContent();
|
||||
uiManager.startCountdown(90);
|
||||
try {
|
||||
await api.sendATCommand(command);
|
||||
} catch (error) {
|
||||
uiManager.showErrorMessage("Error updating USB Modem Protocol");
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
handleDeviceSelection(e) {
|
||||
const selectedMAC = e.target.value;
|
||||
const selectedHostname = e.target.options[e.target.selectedIndex].text;
|
||||
console.log("Selected device:", { mac: selectedMAC, hostname: selectedHostname });
|
||||
|
||||
const isDeviceSelected = selectedMAC !== "Select Device MAC";
|
||||
uiManager.updatePassthroughModeState(isDeviceSelected);
|
||||
}
|
||||
};
|
||||
|
||||
// Application Initialization
|
||||
async function init() {
|
||||
uiManager.showLoadingSpinners();
|
||||
|
||||
try {
|
||||
const [settings, devices] = await Promise.all([
|
||||
api.fetchCurrentSettings(),
|
||||
api.fetchConnectedDevices()
|
||||
]);
|
||||
|
||||
if (settings) {
|
||||
const updateSuccess = await settingsManager.updateSettings(settings);
|
||||
if (updateSuccess) {
|
||||
// Set up event listeners
|
||||
const dnsProxyElement = uiManager.elements.dnsProxy();
|
||||
const ipPassthroughElement = uiManager.elements.ipPassthrough();
|
||||
const usbModemElement = uiManager.elements.usbModem();
|
||||
const connectedDevicesElement = uiManager.elements.connectedDevices();
|
||||
|
||||
if (dnsProxyElement) {
|
||||
dnsProxyElement.addEventListener("change", eventHandlers.handleDnsProxyChange);
|
||||
}
|
||||
if (ipPassthroughElement) {
|
||||
ipPassthroughElement.addEventListener("change", eventHandlers.handleIpPassthroughChange);
|
||||
}
|
||||
if (usbModemElement) {
|
||||
usbModemElement.addEventListener("change", eventHandlers.handleUsbModemProtocolChange);
|
||||
}
|
||||
if (connectedDevicesElement) {
|
||||
connectedDevicesElement.addEventListener("change", eventHandlers.handleDeviceSelection);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (devices) {
|
||||
uiManager.populateConnectedDevices(devices);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Initialization error:", error);
|
||||
uiManager.showErrorMessage("Error initializing settings");
|
||||
} finally {
|
||||
uiManager.hideLoadingSpinners();
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize when DOM is ready
|
||||
document.addEventListener("DOMContentLoaded", init);
|
||||
@@ -0,0 +1,299 @@
|
||||
// api.js - API related functions
|
||||
const api = {
|
||||
async fetch(endpoint, options = {}) {
|
||||
try {
|
||||
const response = await fetch(endpoint, options);
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error(`API Error (${endpoint}):`, error);
|
||||
return null;
|
||||
}
|
||||
},
|
||||
|
||||
async fetchCurrentSettings() {
|
||||
const data = await this.fetch("/cgi-bin/advanced_settings.sh");
|
||||
console.log("Current settings:", data);
|
||||
return data;
|
||||
},
|
||||
|
||||
async fetchConnectedDevices() {
|
||||
const data = await this.fetch("/cgi-bin/fetch_macs.sh");
|
||||
return data;
|
||||
},
|
||||
|
||||
async sendATCommand(command) {
|
||||
return await this.fetch("/cgi-bin/atinout_handler.sh", {
|
||||
method: "POST",
|
||||
body: "command=" + encodeURIComponent(command)
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// uiManager.js - UI related functions
|
||||
const uiManager = {
|
||||
elements: {
|
||||
ipPassthrough: () => document.getElementById("ip-passthrough-mode"),
|
||||
dnsProxy: () => document.getElementById("dns-proxy-mode"),
|
||||
usbModem: () => document.getElementById("usb-modem-protocol"),
|
||||
connectedDevices: () => document.getElementById("connected-devices"),
|
||||
loadingContent: () => document.getElementById("loading-content"),
|
||||
modalButtons: () => document.getElementById("modal-buttons"),
|
||||
countdown: () => document.getElementById("countdown"),
|
||||
rebootModal: () => document.getElementById("reboot-modal"),
|
||||
advancedSettingsIcons: () => document.querySelectorAll(".advanced-settings i")
|
||||
},
|
||||
|
||||
showLoadingSpinners() {
|
||||
this.elements.advancedSettingsIcons().forEach(icon => {
|
||||
icon.classList.add("fa-spinner", "fa-spin");
|
||||
});
|
||||
},
|
||||
|
||||
hideLoadingSpinners() {
|
||||
this.elements.advancedSettingsIcons().forEach(icon => {
|
||||
icon.classList.remove("fa-spinner", "fa-spin");
|
||||
});
|
||||
},
|
||||
|
||||
updatePassthroughModeState(isEnabled) {
|
||||
const select = this.elements.ipPassthrough();
|
||||
if (!select) return;
|
||||
|
||||
const helpText = select.parentElement.querySelector(".help");
|
||||
|
||||
if (isEnabled) {
|
||||
select.removeAttribute("disabled");
|
||||
select.classList.remove("is-warning");
|
||||
if (helpText) {
|
||||
helpText.textContent = "Select a passthrough mode to apply.";
|
||||
helpText.classList.remove("is-warning");
|
||||
helpText.classList.add("is-info");
|
||||
}
|
||||
} else {
|
||||
select.setAttribute("disabled", "disabled");
|
||||
select.classList.add("is-warning");
|
||||
select.value = "Select IP Passthrough Mode";
|
||||
if (helpText) {
|
||||
helpText.textContent = "Please select a device first.";
|
||||
helpText.classList.remove("is-info");
|
||||
helpText.classList.add("is-warning");
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
populateConnectedDevices(devices) {
|
||||
const select = this.elements.connectedDevices();
|
||||
if (!select) {
|
||||
console.error("Connected devices select element not found");
|
||||
return;
|
||||
}
|
||||
|
||||
// Clear existing options except the first one
|
||||
while (select.options.length > 1) {
|
||||
select.remove(1);
|
||||
}
|
||||
|
||||
// Add new options
|
||||
devices.forEach(device => {
|
||||
const option = document.createElement("option");
|
||||
option.value = device.mac;
|
||||
option.textContent = `${device.hostname} - ${device.mac}`;
|
||||
select.appendChild(option);
|
||||
});
|
||||
},
|
||||
|
||||
showModal() {
|
||||
const modal = this.elements.rebootModal();
|
||||
if (modal) {
|
||||
modal.classList.add("is-active");
|
||||
}
|
||||
},
|
||||
|
||||
showLoadingContent() {
|
||||
this.elements.loadingContent().style.display = "flex";
|
||||
this.elements.modalButtons().style.display = "none";
|
||||
this.showModal();
|
||||
},
|
||||
|
||||
startCountdown(duration) {
|
||||
const countdownElement = this.elements.countdown();
|
||||
let countdown = duration;
|
||||
|
||||
const interval = setInterval(() => {
|
||||
countdown--;
|
||||
countdownElement.textContent = countdown;
|
||||
|
||||
if (countdown <= 0) {
|
||||
clearInterval(interval);
|
||||
location.reload();
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
};
|
||||
|
||||
// settingsManager.js - Settings management
|
||||
const settingsManager = {
|
||||
async updateSettings(data) {
|
||||
const elements = {
|
||||
ipPassthrough: uiManager.elements.ipPassthrough(),
|
||||
dnsProxy: uiManager.elements.dnsProxy(),
|
||||
usbModem: uiManager.elements.usbModem()
|
||||
};
|
||||
|
||||
// Validate required elements
|
||||
const missingElements = Object.entries(elements)
|
||||
.filter(([, element]) => !element)
|
||||
.map(([key]) => key);
|
||||
|
||||
if (missingElements.length > 0) {
|
||||
console.error("Missing DOM elements:", missingElements);
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
uiManager.updatePassthroughModeState(false);
|
||||
|
||||
// Update IP Passthrough Mode
|
||||
const mpdnRuleLine = data[0].response.split("\n")[1];
|
||||
if (mpdnRuleLine) {
|
||||
const mpdnRule = mpdnRuleLine.split(":")[1].trim();
|
||||
elements.ipPassthrough.value = this.getPassthroughModeValue(mpdnRule);
|
||||
}
|
||||
|
||||
// Update DNS Proxy
|
||||
const dnsProxyLine = data[1].response.split("\n")[1].split(":")[1].split(",")[1].trim();
|
||||
elements.dnsProxy.value = dnsProxyLine === '"disable"' ? "Disabled" : "Enabled";
|
||||
|
||||
// Update USB Modem Protocol
|
||||
const usbModemProtocolLine = data[2].response.split("\n")[1].split(":")[1].split(",")[1].trim();
|
||||
elements.usbModem.value = this.getUsbModemProtocolValue(usbModemProtocolLine);
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error("Error updating settings:", error);
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
getPassthroughModeValue(mpdnRule) {
|
||||
const modes = {
|
||||
'"MPDN_rule",0,0,0,0,0': "Disabled",
|
||||
'"MPDN_rule",0,1,0,1,1': "ETH Only",
|
||||
'"MPDN_rule",0,1,0,3,1': "USB Only"
|
||||
};
|
||||
return modes[mpdnRule] || "Select IP Passthrough Mode";
|
||||
},
|
||||
|
||||
getUsbModemProtocolValue(protocol) {
|
||||
const protocols = {
|
||||
"0": "RMNET",
|
||||
"1": "ECM (Recommended)",
|
||||
"2": "MBIM",
|
||||
"3": "RNDIS"
|
||||
};
|
||||
return protocols[protocol] || "Select USB Modem Protocol";
|
||||
}
|
||||
};
|
||||
|
||||
// eventHandlers.js - Event handling
|
||||
const eventHandlers = {
|
||||
async handleDnsProxyChange(e) {
|
||||
const selectedMode = e.target.value;
|
||||
const currentMode = e.target.getAttribute("data-current-mode");
|
||||
|
||||
if (selectedMode !== currentMode) {
|
||||
const command = selectedMode === "Enabled"
|
||||
? 'AT+QMAP="DHCPV4DNS","enable"'
|
||||
: 'AT+QMAP="DHCPV4DNS","disable"';
|
||||
await api.sendATCommand(command);
|
||||
}
|
||||
},
|
||||
|
||||
async handleIpPassthroughChange(e) {
|
||||
const selectedMode = e.target.value;
|
||||
const currentMode = e.target.getAttribute("data-current-mode");
|
||||
const selectedDeviceMAC = uiManager.elements.connectedDevices().value;
|
||||
|
||||
if (selectedMode !== currentMode) {
|
||||
const commands = {
|
||||
"Disabled": 'AT+QMPDN="MPDN_rule",0;+CFUN=1,1',
|
||||
"ETH Only": `AT+QMPDN="MPDN_rule",0,1,0,1,1,"${selectedDeviceMAC}"`,
|
||||
"USB Only": `AT+QMPDN="MPDN_rule",0,1,0,3,1,"${selectedDeviceMAC}"`
|
||||
};
|
||||
|
||||
const command = commands[selectedMode];
|
||||
if (command) {
|
||||
uiManager.showLoadingContent();
|
||||
uiManager.startCountdown(80);
|
||||
await api.sendATCommand(command);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
async handleUsbModemProtocolChange(e) {
|
||||
const selectedProtocol = e.target.value;
|
||||
const currentProtocol = e.target.getAttribute("data-current-protocol");
|
||||
|
||||
if (selectedProtocol !== currentProtocol) {
|
||||
const commands = {
|
||||
"RMNET": 'AT+QCFG="usbnet",0;+CFUN=1,1',
|
||||
"ECM (Recommended)": 'AT+QCFG="usbnet",1;+CFUN=1,1',
|
||||
"MBIM": 'AT+QCFG="usbnet",2;+CFUN=1,1',
|
||||
"RNDIS": 'AT+QCFG="usbnet",3;+CFUN=1,1'
|
||||
};
|
||||
|
||||
const command = commands[selectedProtocol];
|
||||
if (command) {
|
||||
uiManager.showLoadingContent();
|
||||
uiManager.startCountdown(80);
|
||||
await api.sendATCommand(command);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
handleDeviceSelection(e) {
|
||||
const selectedMAC = e.target.value;
|
||||
const selectedHostname = e.target.options[e.target.selectedIndex].text;
|
||||
console.log("Selected device:", { mac: selectedMAC, hostname: selectedHostname });
|
||||
|
||||
const isDeviceSelected = selectedMAC !== "Select Device MAC";
|
||||
uiManager.updatePassthroughModeState(isDeviceSelected);
|
||||
}
|
||||
};
|
||||
|
||||
// main.js - Application initialization
|
||||
async function init() {
|
||||
uiManager.showLoadingSpinners();
|
||||
|
||||
try {
|
||||
const [settings, devices] = await Promise.all([
|
||||
api.fetchCurrentSettings(),
|
||||
api.fetchConnectedDevices()
|
||||
]);
|
||||
|
||||
if (settings) {
|
||||
const updateSuccess = await settingsManager.updateSettings(settings);
|
||||
if (updateSuccess) {
|
||||
uiManager.hideLoadingSpinners();
|
||||
|
||||
// Set up event listeners
|
||||
uiManager.elements.dnsProxy().addEventListener("change", eventHandlers.handleDnsProxyChange);
|
||||
uiManager.elements.ipPassthrough().addEventListener("change", eventHandlers.handleIpPassthroughChange);
|
||||
uiManager.elements.usbModem().addEventListener("change", eventHandlers.handleUsbModemProtocolChange);
|
||||
uiManager.elements.connectedDevices().addEventListener("change", eventHandlers.handleDeviceSelection);
|
||||
}
|
||||
}
|
||||
|
||||
if (devices) {
|
||||
uiManager.populateConnectedDevices(devices);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Initialization error:", error);
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize when DOM is ready
|
||||
document.addEventListener("DOMContentLoaded", init);
|
||||
@@ -0,0 +1,94 @@
|
||||
// TTL Control functionality
|
||||
const TTLControl = {
|
||||
async getCurrentState() {
|
||||
try {
|
||||
const response = await fetch('/cgi-bin/advance/ttl.sh');
|
||||
const data = await response.json();
|
||||
return {
|
||||
isEnabled: data.isEnabled,
|
||||
currentValue: data.currentValue || 0
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error fetching TTL state:', error);
|
||||
return { isEnabled: false, currentValue: 0 };
|
||||
}
|
||||
},
|
||||
|
||||
async setTTLValue(value) {
|
||||
try {
|
||||
const response = await fetch('/cgi-bin/advance/ttl.sh', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
body: `ttl=${value}`
|
||||
});
|
||||
const result = await response.json();
|
||||
return result.success;
|
||||
} catch (error) {
|
||||
console.error('Error setting TTL value:', error);
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
updateUI(isEnabled, value) {
|
||||
const stateInput = document.getElementById('ttl-state');
|
||||
const valueInput = document.getElementById('ttl-current-value');
|
||||
const stateIcon = stateInput.nextElementSibling.querySelector('i');
|
||||
const valueIcon = valueInput.nextElementSibling.querySelector('i');
|
||||
|
||||
// Update State UI
|
||||
if (isEnabled) {
|
||||
// Enabled state
|
||||
stateInput.value = 'Enabled';
|
||||
stateInput.classList.remove('has-text-warning', 'is-danger');
|
||||
stateInput.classList.add('has-text-success', 'has-text-weight-bold');
|
||||
stateIcon.classList.remove('fa-exclamation-triangle', 'has-text-warning');
|
||||
stateIcon.classList.add('fa-check', 'has-text-success');
|
||||
} else {
|
||||
// Disabled state
|
||||
stateInput.value = 'Disabled';
|
||||
stateInput.classList.remove('has-text-success', 'is-danger');
|
||||
stateInput.classList.add('has-text-warning', 'has-text-weight-bold');
|
||||
stateIcon.classList.remove('fa-check', 'has-text-success');
|
||||
stateIcon.classList.add('fa-exclamation-triangle', 'has-text-warning');
|
||||
}
|
||||
|
||||
// Update Value UI
|
||||
valueInput.value = value.toString();
|
||||
valueInput.classList.add('has-text-weight-bold', 'has-text-white');
|
||||
if (isEnabled) {
|
||||
valueIcon.classList.remove('fa-exclamation-triangle', 'has-text-warning');
|
||||
valueIcon.classList.add('fa-check', 'has-text-success');
|
||||
} else {
|
||||
valueIcon.classList.remove('fa-check', 'has-text-success');
|
||||
valueIcon.classList.add('fa-exclamation-triangle', 'has-text-warning');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Event Listeners
|
||||
document.addEventListener('DOMContentLoaded', async function() {
|
||||
// Initial state fetch
|
||||
const { isEnabled, currentValue } = await TTLControl.getCurrentState();
|
||||
TTLControl.updateUI(isEnabled, currentValue);
|
||||
|
||||
// Submit button event listener
|
||||
document.getElementById('ttl-submit').addEventListener('click', async function() {
|
||||
const newValue = document.getElementById('ttl-set-value').value;
|
||||
const numValue = parseInt(newValue);
|
||||
|
||||
if (isNaN(numValue) || numValue < 0) {
|
||||
alert('Please enter a valid TTL value (0 or positive number)');
|
||||
return;
|
||||
}
|
||||
|
||||
const success = await TTLControl.setTTLValue(numValue);
|
||||
if (success) {
|
||||
TTLControl.updateUI(numValue !== 0, numValue);
|
||||
alert('TTL settings updated successfully');
|
||||
} else {
|
||||
alert('Failed to update TTL settings');
|
||||
}
|
||||
});
|
||||
});
|
||||
150
ipk-source/sdxpinn-quecmanager/root/www/js/auth/auth.js
Normal file
150
ipk-source/sdxpinn-quecmanager/root/www/js/auth/auth.js
Normal file
@@ -0,0 +1,150 @@
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
const SESSION_DURATION = 30 * 60 * 1000; // 30 minutes in milliseconds
|
||||
|
||||
function generateAuthToken(length = 32) {
|
||||
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
|
||||
return Array.from(crypto.getRandomValues(new Uint8Array(length)))
|
||||
.map(x => charset[x % charset.length])
|
||||
.join('');
|
||||
}
|
||||
|
||||
function getSessionData() {
|
||||
const sessionStr = localStorage.getItem("session");
|
||||
if (!sessionStr) return null;
|
||||
|
||||
try {
|
||||
return JSON.parse(sessionStr);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function setSessionData(token) {
|
||||
const session = {
|
||||
token,
|
||||
lastActivity: Date.now(),
|
||||
expiresAt: Date.now() + SESSION_DURATION
|
||||
};
|
||||
localStorage.setItem("session", JSON.stringify(session));
|
||||
}
|
||||
|
||||
function isSessionValid() {
|
||||
const session = getSessionData();
|
||||
if (!session) return false;
|
||||
|
||||
const now = Date.now();
|
||||
|
||||
// Check if session has expired
|
||||
if (now > session.expiresAt) {
|
||||
logout();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Extend session if it's been more than 5 minutes since last activity
|
||||
if (now - session.lastActivity > 5 * 60 * 1000) {
|
||||
setSessionData(session.token);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function logout() {
|
||||
localStorage.removeItem("session");
|
||||
window.location.href = "index.html";
|
||||
}
|
||||
|
||||
// Initially hide the body to prevent content from flashing
|
||||
document.body.style.display = "none";
|
||||
|
||||
// Define which pages should be protected
|
||||
const protectedPages = [
|
||||
"/home.html",
|
||||
"/advance-settings.html",
|
||||
"/bandlock.html",
|
||||
"/cell-locking.html",
|
||||
"/cell-scanner.html",
|
||||
"/cell-settings.html",
|
||||
"/cell-sms.html",
|
||||
"/about.html",
|
||||
];
|
||||
|
||||
const currentPage = window.location.pathname;
|
||||
|
||||
// Authentication check
|
||||
const isAuthenticated = isSessionValid();
|
||||
|
||||
// Redirect logic
|
||||
if (!isAuthenticated && protectedPages.includes(currentPage)) {
|
||||
window.location.href = "index.html";
|
||||
return;
|
||||
}
|
||||
|
||||
if (isAuthenticated && currentPage.includes("index.html")) {
|
||||
window.location.href = "home.html";
|
||||
return;
|
||||
}
|
||||
|
||||
// Show the page if authentication check is complete
|
||||
document.body.style.display = "";
|
||||
|
||||
// Login form logic
|
||||
const loginForm = document.getElementById("loginForm");
|
||||
if (loginForm) {
|
||||
loginForm.addEventListener("submit", async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
const username = document.getElementById("username").value;
|
||||
const password = document.getElementById("password").value;
|
||||
const errorElement = document.getElementById("error");
|
||||
|
||||
try {
|
||||
const formData = new URLSearchParams();
|
||||
formData.append("username", username);
|
||||
formData.append("password", encodeURIComponent(password));
|
||||
|
||||
const response = await fetch("/cgi-bin/auth.sh", {
|
||||
method: "POST",
|
||||
body: formData,
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
},
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.state === "success") {
|
||||
const newToken = generateAuthToken();
|
||||
setSessionData(newToken);
|
||||
window.location.href = "home.html";
|
||||
} else {
|
||||
errorElement.textContent = "Invalid username or password";
|
||||
}
|
||||
} catch (error) {
|
||||
errorElement.textContent = "An error occurred. Please try again later.";
|
||||
console.error("Login error:", error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Event listeners
|
||||
const logoutButton = document.getElementById("logoutButton");
|
||||
if (logoutButton) {
|
||||
logoutButton.addEventListener("click", logout);
|
||||
}
|
||||
|
||||
document.querySelectorAll(".navbar-item").forEach((el) => {
|
||||
if (el.textContent.includes("Home")) {
|
||||
el.addEventListener("click", (e) => {
|
||||
if (isSessionValid()) {
|
||||
e.preventDefault();
|
||||
window.location.href = "home.html";
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Periodic session check
|
||||
if (protectedPages.includes(currentPage)) {
|
||||
setInterval(isSessionValid, 60000); // Check every minute
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,214 @@
|
||||
// api.js - API related functions
|
||||
const api = {
|
||||
async sendCommand(command) {
|
||||
try {
|
||||
const response = await fetch("/cgi-bin/atinout_handler.sh", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
},
|
||||
body: `command=${encodeURIComponent(command)}`,
|
||||
});
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error("API Error:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// bandManager.js - Band management functionality
|
||||
const bandManager = {
|
||||
async fetchCurrentBands() {
|
||||
try {
|
||||
const data = await api.sendCommand("AT+QCAINFO");
|
||||
const lteBands = data.output.match(/LTE BAND ([0-9]+)/g) || [];
|
||||
const nrBands = data.output.match(/NR5G BAND ([0-9]+)/g) || [];
|
||||
|
||||
const currentBandsElement = document.getElementById("currentBands");
|
||||
if (!currentBandsElement) return;
|
||||
|
||||
if (lteBands.length === 0 && nrBands.length === 0) {
|
||||
currentBandsElement.textContent = "No active bands found";
|
||||
} else if (lteBands.length === 0) {
|
||||
currentBandsElement.textContent = nrBands.join(", ");
|
||||
} else if (nrBands.length === 0) {
|
||||
currentBandsElement.textContent = lteBands.join(", ");
|
||||
} else {
|
||||
currentBandsElement.textContent = [...lteBands, ...nrBands].join(", ");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching current bands:", error);
|
||||
}
|
||||
},
|
||||
|
||||
async fetchSupportedBands() {
|
||||
try {
|
||||
const data = await api.sendCommand('AT+QNWPREFCFG="policy_band"');
|
||||
|
||||
const matches = {
|
||||
lte: data.output.match(/"lte_band",([0-9:]+)/),
|
||||
nsa: data.output.match(/"nsa_nr5g_band",([0-9:]+)/),
|
||||
saDc: data.output.match(/"nrdc_nr5g_band",([0-9:]+)/)
|
||||
};
|
||||
|
||||
if (matches.lte) this.populateBands(matches.lte[1], "#lte_bands");
|
||||
if (matches.nsa) this.populateBands(matches.nsa[1], "#nsa_bands");
|
||||
if (matches.saDc) this.populateBands(matches.saDc[1], "#sanrdc_bands");
|
||||
|
||||
await this.fetchActiveBands();
|
||||
} catch (error) {
|
||||
console.error("Error fetching supported bands:", error);
|
||||
}
|
||||
},
|
||||
|
||||
async fetchActiveBands() {
|
||||
try {
|
||||
const command = 'AT+QNWPREFCFG="lte_band";+QNWPREFCFG="nsa_nr5g_band";+QNWPREFCFG="nr5g_band";+QNWPREFCFG="nrdc_nr5g_band"';
|
||||
const data = await api.sendCommand(command);
|
||||
|
||||
const output = data.output.split("\n").slice(1).join("\n").replace("OK", "");
|
||||
|
||||
const matches = {
|
||||
lte: output.match(/"lte_band",([0-9:]+)/),
|
||||
nsa: output.match(/"nsa_nr5g_band",([0-9:]+)/),
|
||||
saDc: output.split("\n")[6]?.match(/"nr5g_band",([0-9:]+)/)
|
||||
};
|
||||
|
||||
if (matches.lte) this.markActiveBands(matches.lte[1].split(":"), "#lte_bands");
|
||||
if (matches.nsa) this.markActiveBands(matches.nsa[1].split(":"), "#nsa_bands");
|
||||
if (matches.saDc) this.markActiveBands(matches.saDc[1].split(":"), "#sanrdc_bands");
|
||||
|
||||
await this.fetchCurrentBands();
|
||||
} catch (error) {
|
||||
console.error("Error fetching active bands:", error);
|
||||
}
|
||||
},
|
||||
|
||||
populateBands(bandsString, targetId) {
|
||||
const container = document.querySelector(targetId);
|
||||
if (!container) return;
|
||||
|
||||
const html = bandsString.split(":").map(band => `
|
||||
<div class="cell">
|
||||
<label class="checkbox">
|
||||
<input type="checkbox" value="${band}" /> B${band}
|
||||
</label>
|
||||
</div>
|
||||
`).join("");
|
||||
|
||||
container.innerHTML = html;
|
||||
},
|
||||
|
||||
markActiveBands(activeBands, targetId) {
|
||||
document.querySelectorAll(`${targetId} input[type="checkbox"]`).forEach(checkbox => {
|
||||
if (activeBands.includes(checkbox.value)) {
|
||||
checkbox.setAttribute("checked", "checked");
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
uncheckAll(targetId) {
|
||||
document.querySelectorAll(`${targetId} input[type="checkbox"]`).forEach(checkbox => {
|
||||
checkbox.removeAttribute("checked");
|
||||
checkbox.checked = false;
|
||||
});
|
||||
},
|
||||
|
||||
async lockBands(targetId, commandType) {
|
||||
const checkboxes = document.querySelectorAll(`${targetId} input[type="checkbox"]:checked`);
|
||||
const checkedBands = Array.from(checkboxes)
|
||||
.map(cb => cb.value)
|
||||
.sort((a, b) => a - b);
|
||||
|
||||
if (checkedBands.length === 0) {
|
||||
alert("Please select at least one band to lock.");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const command = `AT+QNWPREFCFG="${commandType}",${checkedBands.join(":")}`;
|
||||
await api.sendCommand(command);
|
||||
alert(`Successfully locked ${commandType.split("_")[0].toUpperCase()} bands`);
|
||||
await this.fetchActiveBands();
|
||||
} catch (error) {
|
||||
alert(`Failed to lock bands: ${error.message}`);
|
||||
}
|
||||
},
|
||||
|
||||
async resetBands(targetId, bandType) {
|
||||
const checkboxes = document.querySelectorAll(`${targetId} input[type="checkbox"]`);
|
||||
const selectedBands = [];
|
||||
|
||||
checkboxes.forEach(checkbox => {
|
||||
checkbox.setAttribute("checked", "checked");
|
||||
checkbox.checked = true;
|
||||
selectedBands.push(checkbox.value);
|
||||
});
|
||||
|
||||
try {
|
||||
const command = `AT+QNWPREFCFG="${bandType}",${selectedBands.join(":")}`;
|
||||
await api.sendCommand(command);
|
||||
await this.fetchActiveBands();
|
||||
} catch (error) {
|
||||
console.error(`Error resetting ${bandType}:`, error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// eventHandlers.js - Event handling setup
|
||||
function setupEventListeners() {
|
||||
const handlers = {
|
||||
uncheck: {
|
||||
"uncheckLte": "#lte_bands",
|
||||
"uncheckNsa": "#nsa_bands",
|
||||
"uncheckSaDc": "#sanrdc_bands"
|
||||
},
|
||||
lock: {
|
||||
"lockLte": ["#lte_bands", "lte_band"],
|
||||
"lockNsa": ["#nsa_bands", "nsa_nr5g_band"],
|
||||
"lockSaDc": ["#sanrdc_bands", "nrdc_nr5g_band"]
|
||||
},
|
||||
reset: {
|
||||
"resetLte": ["#lte_bands", "lte_band"],
|
||||
"resetNsa": ["#nsa_bands", "nsa_nr5g_band"],
|
||||
"resetSaDc": ["#sanrdc_bands", "nrdc_nr5g_band"]
|
||||
}
|
||||
};
|
||||
|
||||
// Setup uncheck handlers
|
||||
Object.entries(handlers.uncheck).forEach(([id, targetId]) => {
|
||||
const element = document.getElementById(id);
|
||||
if (element) {
|
||||
element.addEventListener("click", (e) => {
|
||||
e.preventDefault();
|
||||
bandManager.uncheckAll(targetId);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Setup lock handlers
|
||||
Object.entries(handlers.lock).forEach(([id, [targetId, commandType]]) => {
|
||||
const element = document.getElementById(id);
|
||||
if (element) {
|
||||
element.addEventListener("click", () => bandManager.lockBands(targetId, commandType));
|
||||
}
|
||||
});
|
||||
|
||||
// Setup reset handlers
|
||||
Object.entries(handlers.reset).forEach(([id, [targetId, bandType]]) => {
|
||||
const element = document.getElementById(id);
|
||||
if (element) {
|
||||
element.addEventListener("click", (e) => {
|
||||
e.preventDefault();
|
||||
bandManager.resetBands(targetId, bandType);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// main.js - Application initialization
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
setupEventListeners();
|
||||
bandManager.fetchSupportedBands();
|
||||
});
|
||||
@@ -0,0 +1,179 @@
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const form = document.getElementById('apnProfileForm');
|
||||
|
||||
// Helper function to show notifications
|
||||
function showNotification(message, isError = false) {
|
||||
// Remove existing notification if any
|
||||
const existingNotification = form.previousElementSibling;
|
||||
if (existingNotification && existingNotification.classList.contains('notification')) {
|
||||
existingNotification.remove();
|
||||
}
|
||||
|
||||
const notification = document.createElement('div');
|
||||
notification.className = `notification ${isError ? 'is-danger' : 'is-success'} is-light`;
|
||||
notification.innerHTML = `
|
||||
<button class="delete"></button>
|
||||
${message}
|
||||
`;
|
||||
|
||||
form.insertAdjacentElement('beforebegin', notification);
|
||||
|
||||
// Remove notification after 5 seconds
|
||||
setTimeout(() => notification.remove(), 5000);
|
||||
|
||||
// Allow manual close
|
||||
notification.querySelector('.delete').addEventListener('click', () => notification.remove());
|
||||
}
|
||||
|
||||
// Function to validate ICCID format
|
||||
function validateICCID(iccid) {
|
||||
return /^\d{19,20}$/.test(iccid);
|
||||
}
|
||||
|
||||
// Function to validate APN format
|
||||
function validateAPN(apn) {
|
||||
return /^[a-zA-Z0-9.-]+$/.test(apn);
|
||||
}
|
||||
|
||||
// Function to set select element value
|
||||
function setSelectValue(selectElement, value) {
|
||||
const options = selectElement.options;
|
||||
for (let i = 0; i < options.length; i++) {
|
||||
if (options[i].value === value) {
|
||||
selectElement.selectedIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Function to fetch and display existing profiles
|
||||
function fetchProfiles() {
|
||||
fetch('/cgi-bin/cell-settings/fetch-apn-profiles.sh')
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.status === 'success') {
|
||||
// Fill Profile 1
|
||||
if (data.profiles.profile1) {
|
||||
const p1 = data.profiles.profile1;
|
||||
if (p1.iccid) document.getElementById('iccidProfile1').value = p1.iccid;
|
||||
if (p1.apn) document.getElementById('apnProfile1').value = p1.apn;
|
||||
if (p1.pdpType) setSelectValue(document.getElementById('apnPDPType1'), p1.pdpType);
|
||||
}
|
||||
|
||||
// Fill Profile 2
|
||||
if (data.profiles.profile2) {
|
||||
const p2 = data.profiles.profile2;
|
||||
if (p2.iccid) document.getElementById('iccidProfile2').value = p2.iccid;
|
||||
if (p2.apn) document.getElementById('apnProfile2').value = p2.apn;
|
||||
if (p2.pdpType) setSelectValue(document.getElementById('apnPDPType2'), p2.pdpType);
|
||||
}
|
||||
} else {
|
||||
showNotification('No existing profiles found', true);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
showNotification('Error fetching profiles: ' + error.message, true);
|
||||
});
|
||||
}
|
||||
|
||||
// Function to validate form
|
||||
function validateForm() {
|
||||
const iccid1 = document.getElementById('iccidProfile1').value;
|
||||
const apn1 = document.getElementById('apnProfile1').value;
|
||||
const pdp1 = document.getElementById('apnPDPType1').value;
|
||||
|
||||
const iccid2 = document.getElementById('iccidProfile2').value;
|
||||
const apn2 = document.getElementById('apnProfile2').value;
|
||||
const pdp2 = document.getElementById('apnPDPType2').value;
|
||||
|
||||
// Validate first profile (required)
|
||||
if (!iccid1 || !apn1 || pdp1 === 'Select APN PDP Type') {
|
||||
showNotification('Please fill in all fields for Profile 1', true);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!validateICCID(iccid1)) {
|
||||
showNotification('Invalid ICCID format in Profile 1 (should be 19-20 digits)', true);
|
||||
return false;
|
||||
}
|
||||
if (!validateAPN(apn1)) {
|
||||
showNotification('Invalid APN format in Profile 1 (alphanumeric, dots, and hyphens only)', true);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Validate second profile only if any field is filled
|
||||
if (iccid2 || apn2 || pdp2 !== 'Select APN PDP Type') {
|
||||
if (!validateICCID(iccid2)) {
|
||||
showNotification('Invalid ICCID format in Profile 2 (should be 19-20 digits)', true);
|
||||
return false;
|
||||
}
|
||||
if (!validateAPN(apn2)) {
|
||||
showNotification('Invalid APN format in Profile 2 (alphanumeric, dots, and hyphens only)', true);
|
||||
return false;
|
||||
}
|
||||
if (pdp2 === 'Select APN PDP Type') {
|
||||
showNotification('Please select PDP type for Profile 2', true);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Handle form submission
|
||||
document.getElementById('saveAPNProfile').addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
if (!validateForm()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const formData = {
|
||||
iccidProfile1: document.getElementById('iccidProfile1').value,
|
||||
apnProfile1: document.getElementById('apnProfile1').value,
|
||||
pdpType1: document.getElementById('apnPDPType1').value,
|
||||
iccidProfile2: document.getElementById('iccidProfile2').value || '',
|
||||
apnProfile2: document.getElementById('apnProfile2').value || '',
|
||||
pdpType2: document.getElementById('apnPDPType2').value || 'IP' // Default value if not selected
|
||||
};
|
||||
|
||||
// Send data to the server
|
||||
fetch('/cgi-bin/cell-settings/apn-profile.sh', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
body: Object.keys(formData).map(key => {
|
||||
return encodeURIComponent(key) + '=' + encodeURIComponent(formData[key])
|
||||
}).join('&')
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.status === 'success') {
|
||||
showNotification('APN profiles saved successfully');
|
||||
} else {
|
||||
showNotification(data.message || 'Error saving APN profiles', true);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
showNotification('Error saving APN profiles: ' + error.message, true);
|
||||
});
|
||||
});
|
||||
|
||||
// Handle reset button
|
||||
document.getElementById('resetAPNProfile').addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
document.getElementById('iccidProfile1').value = '';
|
||||
document.getElementById('apnProfile1').value = '';
|
||||
document.getElementById('apnPDPType1').selectedIndex = 0;
|
||||
document.getElementById('iccidProfile2').value = '';
|
||||
document.getElementById('apnProfile2').value = '';
|
||||
document.getElementById('apnPDPType2').selectedIndex = 0;
|
||||
|
||||
showNotification('Form has been reset');
|
||||
});
|
||||
|
||||
// Fetch existing profiles when the page loads
|
||||
fetchProfiles();
|
||||
});
|
||||
@@ -0,0 +1,310 @@
|
||||
// State variables to track current values
|
||||
let currentSettings = {
|
||||
apn: "",
|
||||
pdpType: "",
|
||||
};
|
||||
|
||||
let updatedSettings = {
|
||||
apn: "",
|
||||
pdpType: "",
|
||||
};
|
||||
|
||||
let updatedNetworkMode = "";
|
||||
let currentNetworkMode = "";
|
||||
|
||||
let currentNr5GModeControl = "";
|
||||
let updatedNr5GModeControl = "";
|
||||
|
||||
// Function to check if settings have changed
|
||||
function haveSettingsChanged() {
|
||||
return (
|
||||
currentSettings.apn !== updatedSettings.apn ||
|
||||
currentSettings.pdpType !== updatedSettings.pdpType
|
||||
);
|
||||
}
|
||||
|
||||
// Function to check if network mode has changed
|
||||
function haveNetworkModeChanged() {
|
||||
console.log("Current network mode:", currentNetworkMode);
|
||||
console.log("Updated network mode:", updatedNetworkMode);
|
||||
return currentNetworkMode !== updatedNetworkMode;
|
||||
}
|
||||
|
||||
// Function to check if NR5G mode control has changed
|
||||
function haveNr5GModeControlChanged() {
|
||||
console.log("Current NR5G mode control:", currentNr5GModeControl);
|
||||
console.log("Updated NR5G mode control:", updatedNr5GModeControl);
|
||||
return currentNr5GModeControl !== updatedNr5GModeControl;
|
||||
}
|
||||
|
||||
// Function to apply network mode changes immediately
|
||||
async function applyNetworkModeChange() {
|
||||
if (!haveNetworkModeChanged()) {
|
||||
alert("No changes detected in the network mode.");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const atCommand = `AT+QNWPREFCFG="mode_pref",${updatedNetworkMode}`;
|
||||
console.log("Sending AT command for network mode change:", atCommand);
|
||||
const response = await sendATCommand(atCommand);
|
||||
console.log("AT command response:", response);
|
||||
alert("Network mode applied successfully!");
|
||||
} catch (error) {
|
||||
console.error("Error applying network mode:", error);
|
||||
alert("Error applying network mode. Please try again.");
|
||||
}
|
||||
}
|
||||
|
||||
// Function to apply NR5G mode control changes immediately
|
||||
async function applyNr5GModeControlChange() {
|
||||
if (!haveNr5GModeControlChanged()) {
|
||||
alert("No changes detected in the NR5G mode control.");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const atCommand = `AT+QNWPREFCFG="nr5g_disable_mode",${updatedNr5GModeControl}`;
|
||||
console.log("Sending AT command for NR5G mode control change:", atCommand);
|
||||
const response = await sendATCommand(atCommand);
|
||||
console.log("AT command response:", response);
|
||||
alert("NR5G mode control applied successfully!");
|
||||
} catch (error) {
|
||||
console.error("Error applying NR5G mode control:", error);
|
||||
alert("Error applying NR5G mode control. Please try again.");
|
||||
}
|
||||
}
|
||||
|
||||
// Function to send settings to the modem
|
||||
async function saveSettings() {
|
||||
if (!haveSettingsChanged()) {
|
||||
alert("No changes detected in the settings.");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const atCommand = `AT+QMBNCFG="AutoSel",0;+CGDCONT=1,"${updatedSettings.pdpType}","${updatedSettings.apn}"`;
|
||||
console.log("Sending AT command:", atCommand);
|
||||
|
||||
// Disable the input fields while the settings are being saved
|
||||
const inputs = document.querySelectorAll("input, select");
|
||||
inputs.forEach((input) => {
|
||||
input.disabled = true;
|
||||
});
|
||||
const response = await sendATCommand(atCommand);
|
||||
console.log("AT command response:", response);
|
||||
|
||||
await sendATCommand(`AT+COPS=2`);
|
||||
// Wait for 2 seconds before turning on the modem
|
||||
await new Promise((resolve) => setTimeout(resolve, 2000));
|
||||
await sendATCommand(`AT+COPS=0`);
|
||||
|
||||
// Re-enable the input fields after the settings are saved
|
||||
inputs.forEach((input) => {
|
||||
input.disabled = false;
|
||||
});
|
||||
|
||||
// Update current settings after successful save
|
||||
currentSettings = { ...updatedSettings };
|
||||
alert("Settings saved successfully!");
|
||||
} catch (error) {
|
||||
console.error("Error saving settings:", error);
|
||||
alert("Error saving settings. Please try again.");
|
||||
}
|
||||
}
|
||||
|
||||
async function resetAPN() {
|
||||
atCommand = `AT+QMBNCFG="AutoSel",1`;
|
||||
console.log("Sending AT command:", atCommand);
|
||||
|
||||
try {
|
||||
const response = await sendATCommand(atCommand);
|
||||
console.log("AT command response:", response);
|
||||
|
||||
// Restart connection after resetting APN settings
|
||||
await sendATCommand("AT+COPS=2");
|
||||
// Wait for 2 seconds before turning on the modem
|
||||
await new Promise((resolve) => setTimeout(resolve, 2000));
|
||||
await sendATCommand("AT+COPS=0");
|
||||
alert("APN settings reset successfully!");
|
||||
} catch (error) {
|
||||
console.error("Error resetting APN settings:", error);
|
||||
alert("Error resetting APN settings. Please try again.");
|
||||
}
|
||||
}
|
||||
|
||||
async function sendATCommand(command) {
|
||||
try {
|
||||
const response = await fetch("/cgi-bin/atinout_handler.sh", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
},
|
||||
body: "command=" + encodeURIComponent(command),
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
const data = await response.json();
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error("Error sending AT command:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Function to fetch cell settings data
|
||||
async function fetchCellSettings() {
|
||||
try {
|
||||
const response = await fetch("/cgi-bin/cell-settings/cell-settings.sh");
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
console.log("Full response:", data);
|
||||
|
||||
data.forEach((item) => {
|
||||
if (item.response.includes("CGDCONT?")) {
|
||||
const apn = item.response
|
||||
.split("\n")[1]
|
||||
.split(":")[1]
|
||||
.split(",")[2]
|
||||
.replace(/"/g, "")
|
||||
.trim();
|
||||
|
||||
currentSettings.apn = apn;
|
||||
updatedSettings.apn = apn;
|
||||
|
||||
const apnInput = document.getElementById("currentAPN");
|
||||
if (apnInput) {
|
||||
apnInput.value = apn;
|
||||
|
||||
// Add event listener for APN changes
|
||||
if (!apnInput.hasListener) {
|
||||
apnInput.hasListener = true;
|
||||
apnInput.addEventListener("input", (e) => {
|
||||
updatedSettings.apn = e.target.value;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const pdpType = item.response
|
||||
.split("\n")[1]
|
||||
.split(":")[1]
|
||||
.split(",")[1]
|
||||
.replace(/"/g, "")
|
||||
.trim();
|
||||
|
||||
currentSettings.pdpType = pdpType;
|
||||
updatedSettings.pdpType = pdpType;
|
||||
|
||||
const pdpTypeSelect = document.getElementById("apnPDP");
|
||||
if (pdpTypeSelect) {
|
||||
// Set initial value
|
||||
pdpTypeSelect.value =
|
||||
pdpType === "IPV4V6"
|
||||
? "IPV4V6"
|
||||
: pdpType === "IPV6"
|
||||
? "IPV6"
|
||||
: pdpType === "PPP"
|
||||
? "PPP"
|
||||
: "IP";
|
||||
|
||||
// Add event listener for PDP type changes
|
||||
if (!pdpTypeSelect.hasListener) {
|
||||
pdpTypeSelect.hasListener = true;
|
||||
pdpTypeSelect.addEventListener("change", (e) => {
|
||||
updatedSettings.pdpType = e.target.value;
|
||||
});
|
||||
}
|
||||
}
|
||||
} else if (item.response.includes("mode_pref")) {
|
||||
const networkMode = item.response
|
||||
.split("\n")[1]
|
||||
.replace("+QNWPREFCFG: ", "")
|
||||
.split(",")[1]
|
||||
.trim();
|
||||
|
||||
currentNetworkMode = networkMode;
|
||||
updatedNetworkMode = networkMode;
|
||||
|
||||
console.log("Network mode:", networkMode);
|
||||
|
||||
const networkSelect = document.getElementById("networkPreference");
|
||||
if (networkSelect) {
|
||||
// Set initial value based on actual value from modem
|
||||
networkSelect.value =
|
||||
networkMode === "LTE:NR5G"
|
||||
? "LTE:NR5G"
|
||||
: networkMode === "NR5G"
|
||||
? "NR5G"
|
||||
: networkMode === "LTE"
|
||||
? "LTE"
|
||||
: "AUTO";
|
||||
|
||||
// Add event listener for network mode changes, if there is, run applyNetworkModeChange
|
||||
if (!networkSelect.hasListener) {
|
||||
networkSelect.hasListener = true;
|
||||
networkSelect.addEventListener("change", (e) => {
|
||||
updatedNetworkMode = e.target.value;
|
||||
applyNetworkModeChange();
|
||||
});
|
||||
}
|
||||
}
|
||||
} else if (item.response.includes("nr5g_disable_mode")) {
|
||||
const nr5GModeControl = item.response
|
||||
.split("\n")[1]
|
||||
.split(":")[1]
|
||||
.split(",")[1]
|
||||
.trim();
|
||||
|
||||
console.log("NR5G mode control:", nr5GModeControl);
|
||||
|
||||
currentNr5GModeControl = nr5GModeControl;
|
||||
updatedNr5GModeControl = nr5GModeControl;
|
||||
|
||||
const nr5GControlSelect = document.getElementById("nr5gModeControl");
|
||||
|
||||
if (nr5GControlSelect) {
|
||||
// Set initial value based on actual value from modem
|
||||
nr5GControlSelect.value =
|
||||
nr5GModeControl === "0" ? "0" : nr5GModeControl === "1" ? "1" : "2";
|
||||
|
||||
// Add event listener for NR5G mode control changes, if there is, run applyNr5GModeControlChange
|
||||
if (!nr5GControlSelect.hasListener) {
|
||||
nr5GControlSelect.hasListener = true;
|
||||
nr5GControlSelect.addEventListener("change", (e) => {
|
||||
updatedNr5GModeControl = e.target.value;
|
||||
applyNr5GModeControlChange();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error fetching cell settings:", error);
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize when DOM is loaded
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
fetchCellSettings();
|
||||
|
||||
// Add event listener for both save buttons
|
||||
const saveButtons = document.querySelectorAll(".card-footer-item");
|
||||
saveButtons.forEach((button) => {
|
||||
if (button.textContent.trim() === "Save APN") {
|
||||
button.addEventListener("click", saveSettings);
|
||||
} else if (button.textContent.trim() === "Reset APN") {
|
||||
button.addEventListener("click", resetAPN);
|
||||
}
|
||||
});
|
||||
|
||||
// For every alert and close button, add event listener to refetch cell settings
|
||||
const alertButtons = document.querySelectorAll(".delete");
|
||||
alertButtons.forEach((button) => {
|
||||
button.addEventListener("click", fetchCellSettings);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,36 @@
|
||||
// Handle form submission via JavaScript
|
||||
document
|
||||
.getElementById("commandForm")
|
||||
.addEventListener("submit", function (e) {
|
||||
e.preventDefault(); // Prevent default form submission
|
||||
|
||||
const commandInput = document.getElementById("command").value;
|
||||
const outputTextarea = document.getElementById("output");
|
||||
|
||||
// Make sure input is not empty
|
||||
if (commandInput.trim() === "") {
|
||||
outputTextarea.value = "Please enter a valid AT command.";
|
||||
return;
|
||||
}
|
||||
|
||||
// Send the AT command to the CGI script via fetch
|
||||
fetch("/cgi-bin/atinout_handler.sh", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
},
|
||||
body: `command=${encodeURIComponent(commandInput)}`,
|
||||
})
|
||||
.then((response) => response.json())
|
||||
.then((data) => {
|
||||
// Display the response in the textarea
|
||||
if (data.output) {
|
||||
outputTextarea.value = data.output;
|
||||
} else {
|
||||
outputTextarea.value = "Error: No output received.";
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
outputTextarea.value = `Error fetching data: ${error.message}`;
|
||||
});
|
||||
});
|
||||
1321
ipk-source/sdxpinn-quecmanager/root/www/js/home/main.js
Normal file
1321
ipk-source/sdxpinn-quecmanager/root/www/js/home/main.js
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,42 @@
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// Functions to open and close a modal
|
||||
function openModal($el) {
|
||||
$el.classList.add('is-active');
|
||||
}
|
||||
|
||||
function closeModal($el) {
|
||||
$el.classList.remove('is-active');
|
||||
}
|
||||
|
||||
function closeAllModals() {
|
||||
(document.querySelectorAll('.modal') || []).forEach(($modal) => {
|
||||
closeModal($modal);
|
||||
});
|
||||
}
|
||||
|
||||
// Add a click event on buttons to open a specific modal
|
||||
(document.querySelectorAll('.reboot-modal') || []).forEach(($trigger) => {
|
||||
const modal = $trigger.dataset.target;
|
||||
const $target = document.getElementById(modal);
|
||||
|
||||
$trigger.addEventListener('click', () => {
|
||||
openModal($target);
|
||||
});
|
||||
});
|
||||
|
||||
// Add a click event on various child elements to close the parent modal
|
||||
(document.querySelectorAll('.modal-background, .modal-close, .modal-card-head .delete, .modal-card-body .cancel') || []).forEach(($close) => {
|
||||
const $target = $close.closest('.modal');
|
||||
|
||||
$close.addEventListener('click', () => {
|
||||
closeModal($target);
|
||||
});
|
||||
});
|
||||
|
||||
// Add a keyboard event to close all modals
|
||||
document.addEventListener('keydown', (event) => {
|
||||
if(event.key === "Escape") {
|
||||
closeAllModals();
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,17 @@
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// Get all "navbar-burger" elements
|
||||
const $navbarBurgers = Array.prototype.slice.call(document.querySelectorAll('.navbar-burger'), 0);
|
||||
|
||||
// Add a click event on each of them
|
||||
$navbarBurgers.forEach(el => {
|
||||
el.addEventListener('click', () => {
|
||||
// Get the target from the "data-target" attribute
|
||||
const target = el.dataset.target;
|
||||
const $target = document.getElementById(target);
|
||||
|
||||
// Toggle the "is-active" class on both the "navbar-burger" and the "navbar-menu"
|
||||
el.classList.toggle('is-active');
|
||||
$target.classList.toggle('is-active');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,11 @@
|
||||
$(document).ready(function() {
|
||||
|
||||
// Check for click events on the navbar burger icon
|
||||
$(".navbar-burger").click(function() {
|
||||
|
||||
// Toggle the "is-active" class on both the "navbar-burger" and the "navbar-menu"
|
||||
$(".navbar-burger").toggleClass("is-active");
|
||||
$(".navbar-menu").toggleClass("is-active");
|
||||
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,27 @@
|
||||
// toggle-theme.js
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
const themeToggleButton = document.querySelector('.js-theme-toggle');
|
||||
const htmlElement = document.documentElement;
|
||||
const icon = themeToggleButton.querySelector('.icon i');
|
||||
|
||||
// Toggle theme on button click
|
||||
themeToggleButton.addEventListener('click', function () {
|
||||
if (htmlElement.classList.contains('theme-dark')) {
|
||||
htmlElement.classList.remove('theme-dark');
|
||||
htmlElement.classList.add('theme-light');
|
||||
localStorage.setItem('theme', 'theme-light');
|
||||
|
||||
// Change icon to moon (light mode)
|
||||
icon.classList.remove('fa-sun');
|
||||
icon.classList.add('fa-moon');
|
||||
} else {
|
||||
htmlElement.classList.remove('theme-light');
|
||||
htmlElement.classList.add('theme-dark');
|
||||
localStorage.setItem('theme', 'theme-dark');
|
||||
|
||||
// Change icon to sun (dark mode)
|
||||
icon.classList.remove('fa-moon');
|
||||
icon.classList.add('fa-sun');
|
||||
}
|
||||
});
|
||||
});
|
||||
132
ipk-source/sdxpinn-quecmanager/root/www/js/utils/reboot.js
Normal file
132
ipk-source/sdxpinn-quecmanager/root/www/js/utils/reboot.js
Normal file
@@ -0,0 +1,132 @@
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const modal = document.getElementById('reboot-modal');
|
||||
const rebootButton = document.getElementById('rebootModem');
|
||||
const cancelButtons = modal.querySelectorAll('.cancel, .modal-background');
|
||||
const powerButton = document.querySelector('div.button.is-warning.is-outlined.reboot-modal');
|
||||
const modalMessage = document.getElementById('modal-message');
|
||||
const loadingContent = document.getElementById('loading-content');
|
||||
const modalButtons = document.getElementById('modal-buttons');
|
||||
const countdownElement = document.getElementById('countdown');
|
||||
|
||||
let countdownInterval;
|
||||
|
||||
function toggleModal(show = true) {
|
||||
modal.classList.toggle('is-active', show);
|
||||
document.documentElement.classList.toggle('is-clipped', show);
|
||||
|
||||
// Reset modal content when closing
|
||||
if (!show) {
|
||||
modalMessage.style.display = 'block';
|
||||
loadingContent.style.display = 'none';
|
||||
modalButtons.style.display = 'flex';
|
||||
if (countdownInterval) {
|
||||
clearInterval(countdownInterval);
|
||||
}
|
||||
countdownElement.textContent = '90';
|
||||
}
|
||||
}
|
||||
|
||||
function startCountdown() {
|
||||
let timeLeft = 90;
|
||||
|
||||
// Update display for countdown
|
||||
modalMessage.style.display = 'none';
|
||||
loadingContent.style.display = 'flex';
|
||||
modalButtons.style.display = 'none';
|
||||
|
||||
countdownInterval = setInterval(() => {
|
||||
timeLeft--;
|
||||
countdownElement.textContent = timeLeft;
|
||||
|
||||
if (timeLeft <= 0) {
|
||||
clearInterval(countdownInterval);
|
||||
window.location.reload();
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
// Show modal when restart connection button is clicked
|
||||
powerButton.addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
toggleModal(true);
|
||||
});
|
||||
|
||||
// Hide modal when cancel or background is clicked
|
||||
cancelButtons.forEach(button => {
|
||||
button.addEventListener('click', () => toggleModal(false));
|
||||
});
|
||||
|
||||
// Handle ESC key press
|
||||
document.addEventListener('keydown', function(e) {
|
||||
if (e.key === 'Escape' && modal.classList.contains('is-active')) {
|
||||
toggleModal(false);
|
||||
}
|
||||
});
|
||||
|
||||
// Function to send AT command
|
||||
async function sendRebootCommand() {
|
||||
try {
|
||||
// Disable the reboot button and show loading state
|
||||
rebootButton.classList.add('is-loading');
|
||||
rebootButton.disabled = true;
|
||||
|
||||
const response = await fetch('/cgi-bin/atinout_handler.sh', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
body: 'command=' + encodeURIComponent('AT+QPOWD=1')
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.output && data.output.includes('OK')) {
|
||||
startCountdown();
|
||||
} else {
|
||||
throw new Error('Reboot command failed');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
toggleModal(false);
|
||||
showNotification('Failed to reboot device. Please try again.', 'is-danger');
|
||||
} finally {
|
||||
// Re-enable the reboot button and remove loading state
|
||||
rebootButton.classList.remove('is-loading');
|
||||
rebootButton.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Function to show notification (for errors only now)
|
||||
function showNotification(message, type = 'is-info') {
|
||||
const notification = document.createElement('div');
|
||||
notification.className = `notification ${type} is-light`;
|
||||
notification.style.position = 'fixed';
|
||||
notification.style.top = '1rem';
|
||||
notification.style.right = '1rem';
|
||||
notification.style.zIndex = '9999';
|
||||
notification.style.maxWidth = '300px';
|
||||
|
||||
const deleteButton = document.createElement('button');
|
||||
deleteButton.className = 'delete';
|
||||
deleteButton.addEventListener('click', () => notification.remove());
|
||||
|
||||
notification.appendChild(deleteButton);
|
||||
notification.appendChild(document.createTextNode(message));
|
||||
|
||||
document.body.appendChild(notification);
|
||||
|
||||
setTimeout(() => {
|
||||
if (document.body.contains(notification)) {
|
||||
notification.remove();
|
||||
}
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
// Handle reboot button click
|
||||
rebootButton.addEventListener('click', sendRebootCommand);
|
||||
});
|
||||
@@ -0,0 +1,69 @@
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const restartBtn = document.getElementById('restartConnectionBtn');
|
||||
|
||||
// Function to send AT commands
|
||||
async function sendRestartCommands() {
|
||||
try {
|
||||
// Disable the restart button and show loading state
|
||||
restartBtn.classList.add('is-loading');
|
||||
restartBtn.disabled = true;
|
||||
|
||||
// Send AT+CFUN=0
|
||||
const response1 = await fetch('/cgi-bin/atinout_handler.sh', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
body: 'command=' + encodeURIComponent('AT+CFUN=0')
|
||||
});
|
||||
|
||||
if (!response1.ok) {
|
||||
throw new Error(`HTTP error! status: ${response1.status}`);
|
||||
}
|
||||
|
||||
// Wait for 3 seconds
|
||||
await new Promise(resolve => setTimeout(resolve, 3000));
|
||||
|
||||
// Send AT+CFUN=1
|
||||
const response2 = await fetch('/cgi-bin/atinout_handler.sh', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
body: 'command=' + encodeURIComponent('AT+CFUN=1')
|
||||
});
|
||||
|
||||
if (!response2.ok) {
|
||||
throw new Error(`HTTP error! status: ${response2.status}`);
|
||||
}
|
||||
|
||||
const data1 = await response1.json();
|
||||
const data2 = await response2.json();
|
||||
|
||||
if (data1.output.includes('OK') && data2.output.includes('OK')) {
|
||||
alert('Connection restarted successfully');
|
||||
// Optionally reload the page after a short delay
|
||||
setTimeout(() => {
|
||||
window.location.reload();
|
||||
}, 1000);
|
||||
} else {
|
||||
throw new Error('Restart command failed');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
alert('Failed to restart connection. Please try again.');
|
||||
} finally {
|
||||
// Re-enable the restart button and remove loading state
|
||||
restartBtn.classList.remove('is-loading');
|
||||
restartBtn.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Add click event listener to the restart button
|
||||
if (restartBtn) {
|
||||
restartBtn.addEventListener('click', sendRestartCommands);
|
||||
} else {
|
||||
console.warn('Restart Connection button not found in the DOM');
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user