diff --git a/Client/.gitignore b/Client/.gitignore new file mode 100644 index 0000000..c74a7af --- /dev/null +++ b/Client/.gitignore @@ -0,0 +1 @@ +*radioPresets.json \ No newline at end of file diff --git a/Client/app.js b/Client/app.js index 09ff507..468cd22 100644 --- a/Client/app.js +++ b/Client/app.js @@ -98,4 +98,4 @@ log.DEBUG(`Starting HTTP Server`); runHTTPServer(); log.DEBUG("Checking in with the master server") - checkIn(); + checkIn(true); diff --git a/Client/config/radioPresets.json b/Client/config/radioPresets.json deleted file mode 100644 index 204fe78..0000000 --- a/Client/config/radioPresets.json +++ /dev/null @@ -1 +0,0 @@ -{"Westchester Cty. Simulcast":{"frequencies":[470575000,470375000,470525000,470575000,470550000],"mode":"p25","trunkFile":"trunk.tsv"},"coppies":{"frequencies":[154690000],"mode":"nbfm","trunkFile":"none"},"poopoo":{"frequencies":[479135500],"mode":"nbfm","trunkFile":"none"},"ppeeeeeeeeee":{"frequencies":[479135500,133990000,133000000,555999000],"mode":"p25","trunkFile":"none"}} \ No newline at end of file diff --git a/Client/config/radioPresets.json.EXAMPLE b/Client/config/radioPresets.json.EXAMPLE new file mode 100644 index 0000000..d33fe51 --- /dev/null +++ b/Client/config/radioPresets.json.EXAMPLE @@ -0,0 +1,18 @@ +{ + "Default P25 System Name": { + "frequencies": [ + 155344000, + 155444000, + 155555000 + ], + "mode": "p25", + "trunkFile": "trunk.tsv" + }, + "Default NBFM System": { + "frequencies": [ + 154690000 + ], + "mode": "nbfm", + "trunkFile": "none" + } +} \ No newline at end of file diff --git a/Client/controllers/clientController.js b/Client/controllers/clientController.js index 8d02999..c8812b4 100644 --- a/Client/controllers/clientController.js +++ b/Client/controllers/clientController.js @@ -5,13 +5,14 @@ const log = new DebugBuilder("client", "clientController"); require('dotenv').config(); const modes = require("../config/modes"); // Modules -const { executeAsyncConsoleCommand, nodeObject, BufferToJson } = require("../utilities/utilities"); +const { executeAsyncConsoleCommand, BufferToJson, nodeObject } = require("../utilities/utilities"); // Utilities -const { updateId, updateConfig } = require("../utilities/updateConfig"); +const { getFullConfig } = require("../utilities/configHandler"); +const { updateId, updateConfig, updateClientConfig } = require("../utilities/updateConfig"); const { updatePreset, addNewPreset, getPresets, removePreset } = require("../utilities/updatePresets"); const { onHttpError, requestOptions, sendHttpRequest } = require("../utilities/httpRequests"); -var runningClientConfig = new nodeObject({_id: process.env.CLIENT_ID, _ip: process.env.CLIENT_IP, _name: process.env.CLIENT_NAME, _port: process.env.CLIENT_PORT, _location: process.env.CLIENT_LOCATION, _nearbySystems: getPresets(), _online: process.env.CLIENT_ONLINE}); +var runningClientConfig = getFullConfig() /** * Check the body for the required fields to update or add a preset @@ -21,11 +22,11 @@ var runningClientConfig = new nodeObject({_id: process.env.CLIENT_ID, _ip: proce * @returns {*} */ function checkBodyForPresetFields(req, res, callback) { - if (!req.body?.systemName) return res.status(403).json({"message": "No system in the request"}); - if (!req.body?.frequencies && Array.isArray(req.body.frequencies)) return res.status(403).json({"message": "No frequencies in the request or type is not an array"}); - if (!req.body?.mode && typeof req.body.mode === "string") return res.status(403).json({"message": "No mode in the request"}); + if (!req.body?.systemName) return res.status(403).json({ "message": "No system in the request" }); + if (!req.body?.frequencies && Array.isArray(req.body.frequencies)) return res.status(403).json({ "message": "No frequencies in the request or type is not an array" }); + if (!req.body?.mode && typeof req.body.mode === "string") return res.status(403).json({ "message": "No mode in the request" }); if (!req.body?.trunkFile) { - if (modes.digitalModes.includes(req.body.mode)) return res.status(403).json({"message": "No trunk file in the request but digital mode specified. If you are not using a trunk file for this frequency make sure to specify 'none' for trunk file in the request"}) + if (modes.digitalModes.includes(req.body.mode)) return res.status(403).json({ "message": "No trunk file in the request but digital mode specified. If you are not using a trunk file for this frequency make sure to specify 'none' for trunk file in the request" }) // If there is a value keep it but if not, add nothing so the system can update that key (if needed) req.body.trunkFile = req.body.trunkFile ?? "none"; } @@ -33,7 +34,7 @@ function checkBodyForPresetFields(req, res, callback) { return callback(); } -async function checkLocalIP() { +async function checkLocalIP() { if (process.platform === "win32") { // Windows var networkConfig = await executeAsyncConsoleCommand("ipconfig"); @@ -71,21 +72,21 @@ exports.checkConfig = async function checkConfig() { await updateId(0); runningClientConfig.id = 0; } - + if (!runningClientConfig.ip) { const ipAddr = await checkLocalIP(); await updateConfig('CLIENT_IP', ipAddr); runningClientConfig.ip = ipAddr; } - if(!runningClientConfig.name) { + if (!runningClientConfig.name) { const lastOctet = await String(checkLocalIP()).spit('.')[-1]; const name = `Radio-Node-${lastOctet}`; await updateConfig('CLIENT_NAME', name); runningClientConfig.name = name; } - if(!runningClientConfig.port) { + if (!runningClientConfig.port) { const port = 3010; await updateConfig('CLIENT_PORT', port); runningClientConfig.port = port; @@ -94,19 +95,20 @@ exports.checkConfig = async function checkConfig() { } /** Check in with the server - * If the bot has a saved ID, check in with the server to update any information or just check back in + * If the bot has a saved ID, check in with the server to get any updated information or just check back in * If the bot does not have a saved ID, it will attempt to request a new ID from the server + * + * @param {boolean} update If set to true, the client will update the server to it's config, instead of taking the server's config */ -exports.checkIn = async () => { +exports.checkIn = async (update = false) => { let reqOptions; await this.checkConfig(); - runningClientConfig.online = true; // Check if there is an ID found, if not add the node to the server. If there was an ID, check in with the server to make sure it has the correct information - try { - if (!runningClientConfig?.id || runningClientConfig.id == 0) { + try { + if (!runningClientConfig?.id || runningClientConfig.id == 0) { // ID was not found in the config, creating a new node - reqOptions = new requestOptions("/nodes/newNode", "POST"); - sendHttpRequest(reqOptions, JSON.stringify(runningClientConfig), async (responseObject) => { + reqOptions = new requestOptions("/nodes/newNode", "POST"); + sendHttpRequest(reqOptions, JSON.stringify(runningClientConfig), async (responseObject) => { // Check if the server responded if (!responseObject) { log.WARN("Server did not respond to checkIn. Will wait 60 seconds then try again"); @@ -118,7 +120,7 @@ exports.checkIn = async () => { } // Update the client's ID if the server accepted its - if (responseObject.statusCode === 202) { + if (responseObject.statusCode === 202) { runningClientConfig.id = responseObject.body.nodeId; log.DEBUG("Response object from new node: ", responseObject, runningClientConfig); await updateId(runningClientConfig.id); @@ -129,12 +131,13 @@ exports.checkIn = async () => { log.DEBUG("HTTP Error: ", responseObject, await BufferToJson(responseObject.body)); await onHttpError(responseObject.statusCode); } - + }); } else { // ID is in the config, checking in with the server - reqOptions = new requestOptions("/nodes/nodeCheckIn", "POST"); + if (update) reqOptions = new requestOptions(`/nodes/${runningClientConfig.id}`, "PUT"); + else reqOptions = new requestOptions(`/nodes/nodeCheckIn/${runningClientConfig.id}`, "POST"); sendHttpRequest(reqOptions, JSON.stringify(runningClientConfig), (responseObject) => { log.DEBUG("Check In Respose: ", responseObject); // Check if the server responded @@ -146,13 +149,17 @@ exports.checkIn = async () => { }, 60000); return } - + if (responseObject.statusCode === 202) { log.DEBUG("Updated keys: ", responseObject.body.updatedKeys) // Server accepted an update } if (responseObject.statusCode === 200) { // Server accepted the response but there were no keys to be updated + if (!update){ + const tempUpdatedConfig = updateClientConfig(responseObject.body); + if (!tempUpdatedConfig.length > 0) return; + } } if (responseObject.statusCode >= 300) { // Server threw an error @@ -174,11 +181,30 @@ exports.requestCheckIn = async (req, res) => { return res.sendStatus(200); } +/** + * Express JS Wrapper for checking and updating client config + * @param {*} req + * @param {*} res + * @returns + */ +exports.updateClientConfigWrapper = async (req, res) => { + // Convert the online status to a boolean to be worked with + log.DEBUG("REQ Body: ", req.body); + const updatedKeys = await updateClientConfig(req.body); + if (updatedKeys) { + log.DEBUG("Keys have been updated, updating running config and checking in with the server: ", updatedKeys); + runningClientConfig = await getFullConfig(); + await this.checkIn(true); + } + res.status(200).json(updatedKeys); +} + /** Controller for the /client/presets endpoint * This is the endpoint wrapper to get the presets object */ exports.getPresets = async (req, res) => { - return res.status(200).json(getPresets()); + runningClientConfig.nearbySystems = getPresets(); + return res.status(200).json(runningClientConfig.nearbySystems); } /** Controller for the /client/updatePreset endpoint @@ -187,8 +213,10 @@ exports.getPresets = async (req, res) => { exports.updatePreset = async (req, res) => { checkBodyForPresetFields(req, res, () => { updatePreset(req.body.systemName, () => { + runningClientConfig.nearbySystems = getPresets(); + this.checkIn(true); return res.sendStatus(200); - }, {frequencies: req.body.frequencies, mode: req.body.mode, trunkFile: req.body.trunkFile}); + }, { frequencies: req.body.frequencies, mode: req.body.mode, trunkFile: req.body.trunkFile }); }) } @@ -198,6 +226,8 @@ exports.updatePreset = async (req, res) => { exports.addNewPreset = async (req, res) => { checkBodyForPresetFields(req, res, () => { addNewPreset(req.body.systemName, req.body.frequencies, req.body.mode, () => { + runningClientConfig.nearbySystems = getPresets(); + this.checkIn(true); return res.sendStatus(200); }, req.body.trunkFile); }); @@ -208,8 +238,10 @@ exports.addNewPreset = async (req, res) => { */ exports.removePreset = async (req, res) => { checkBodyForPresetFields(req, res, () => { - if (!req.body.systemName) return res.status("500").json({"message": "You must specify a system name to delete, this must match exactly to how the system name is saved."}) + if (!req.body.systemName) return res.status("500").json({ "message": "You must specify a system name to delete, this must match exactly to how the system name is saved." }) removePreset(req.body.systemName, () => { + runningClientConfig.nearbySystems = getPresets(); + this.checkIn(true); return res.sendStatus(200); }, req.body.trunkFile); }); diff --git a/Client/public/res/css/main.css b/Client/public/res/css/main.css new file mode 100644 index 0000000..5c44612 --- /dev/null +++ b/Client/public/res/css/main.css @@ -0,0 +1,183 @@ +.node-card { + position: relative; + display: flex; + flex-direction: column; + min-width: 0; + word-wrap: break-word; + background-color: #fff; + background-clip: border-box; + border: 1px solid #eff0f2; + border-radius: 1rem; + margin-bottom: 24px; + box-shadow: 0 2px 3px #e4e8f0; +} + +.avatar-md { + height: 4rem; + width: 4rem; +} + +.rounded-circle { + border-radius: 50% !important; +} + +.img-thumbnail { + padding: 0.25rem; + background-color: #f1f3f7; + border: 1px solid #eff0f2; + border-radius: 0.75rem; +} + +.avatar-title { + align-items: center; + background-color: #3b76e1; + color: #fff; + display: flex; + font-weight: 500; + height: 100%; + justify-content: center; + width: 100%; +} + +.bg-soft-primary { + background-color: rgba(59, 118, 225, .25) !important; +} + +a { + text-decoration: none !important; +} + +.badge-soft-danger { + color: #f56e6e !important; + background-color: rgba(245, 110, 110, .1); +} + +.badge-soft-success { + color: #63ad6f !important; + background-color: rgba(99, 173, 111, .1); +} + +.mb-0 { + margin-bottom: 0 !important; +} + +.badge { + display: inline-block; + padding: 0.25em 0.6em; + font-size: 75%; + font-weight: 500; + line-height: 1; + color: #fff; + text-align: center; + white-space: nowrap; + vertical-align: baseline; + border-radius: 0.75rem; +} + +/* Info Card Section */ +.info-card { + background-color: #fff; + border-radius: 10px; + border: none; + position: relative; + margin-bottom: 30px; + box-shadow: 0 0.46875rem 2.1875rem rgba(90, 97, 105, 0.1), 0 0.9375rem 1.40625rem rgba(90, 97, 105, 0.1), 0 0.25rem 0.53125rem rgba(90, 97, 105, 0.12), 0 0.125rem 0.1875rem rgba(90, 97, 105, 0.1); +} + +.info-card .card-statistic .card-icon-large .bi { + font-size: 110px; +} + +.info-card .card-statistic .card-icon { + text-align: center; + line-height: 50px; + margin-left: 15px; + color: #000; + position: absolute; + right: -5px; + top: 20px; + opacity: 0.1; +} + +/* Info Card Background Colors */ + +.l-bg-cherry { + background: linear-gradient(to right, #493240, #f09) !important; + color: #fff; +} + +.l-bg-blue-dark { + background: linear-gradient(to right, #373b44, #4286f4) !important; + color: #fff; +} + +.l-bg-green-dark { + background: linear-gradient(to right, #0a504a, #38ef7d) !important; + color: #fff; +} + +.l-bg-orange-dark { + background: linear-gradient(to right, #a86008, #ffba56) !important; + color: #fff; +} + +.l-bg-cyan { + background: linear-gradient(135deg, #289cf5, #84c0ec) !important; + color: #fff; +} + +.l-bg-green { + background: linear-gradient(135deg, #23bdb8 0%, #43e794 100%) !important; + color: #fff; +} + +.l-bg-orange { + background: linear-gradient(to right, #f9900e, #ffba56) !important; + color: #fff; +} + +/* Global Section */ +.sidebar-container { + min-height: 95vh; +} + +.sidebar { + position: fixed; + top: 5vh; + bottom: 0; + left: 0; + } + +/* User table section */ + +.label { + border-radius: 3px; + font-size: 1.1em; + font-weight: 600; +} + +.user-list tbody td .user-subhead { + font-size: 1em; + font-style: italic; +} + +.table thead tr th { + text-transform: uppercase; + font-size: 0.875em; +} + +.table thead tr th { + border-bottom: 2px solid #e7ebee; +} + +.table tbody tr td:first-child { + font-size: 1.125em; + font-weight: 300; +} + +.table tbody tr td { + font-size: 0.875em; + vertical-align: middle; + border-top: 1px solid #e7ebee; + padding: 12px 8px; +} diff --git a/Client/public/res/js/node.js b/Client/public/res/js/node.js new file mode 100644 index 0000000..746de7b --- /dev/null +++ b/Client/public/res/js/node.js @@ -0,0 +1,402 @@ +$(document).ready(async () => { + console.log("Loading stored notifications..."); + await loadStoredToasts(); + console.log("Showing stored notifications..."); + await showStoredToasts(); +}); + +/** + * Gets all toasts stored in local storage + * + * @returns {Object} Object of toasts in storage + */ +function getStoredToasts() { + if (localStorage.getItem("toasts")) { + const storedToasts = JSON.parse(localStorage.getItem("toasts")); + console.log("LOADED STORED TOASTS: ", storedToasts); + navbarUpdateNotificationBellCount(storedToasts); + return storedToasts; + } + else return false +} + +/** + * Adds a toast to storage, will not allow duplicates + * + * @param {Date} time The date object from when the toast was created + * @param {*} message The message of the toast + */ +function addToastToStorage(time, message) { + var toasts = [{ 'time': time, 'message': message }] + var storedToasts = getStoredToasts(); + console.log("Adding new notification to storage: ", toasts); + if (storedToasts) { + toasts = toasts.concat(storedToasts); + console.log("Combined new and stored notifications: ", toasts); + toasts = toasts.filter((value, index, self) => + index === self.findIndex((t) => ( + t.time === value.time && t.message === value.message + )) + ) + } + console.log("Deduped stored notifications: ", toasts); + localStorage.setItem("toasts", JSON.stringify(toasts)); + navbarUpdateNotificationBellCount(toasts); +} + +/** + * Removes a toast from the local storage + * + * @param {Date} time The date object from when the toast was created + * @param {*} message The message of the toast + */ +function removeToastFromStorage(time, message) { + const toastToRemove = { 'time': time, 'message': message } + console.log("Toast to remove: ", toastToRemove); + var toasts = getStoredToasts(); + console.log("Stored toasts: ", toasts); + if (toasts.indexOf(toastToRemove)) toasts.splice(toasts.indexOf(toastToRemove) - 1, 1) + console.log("Toasts with selected toast removed: ", toasts); + localStorage.setItem("toasts", JSON.stringify(toasts)); + navbarUpdateNotificationBellCount(toasts); +} + +/** + * Shows all stored toasts + */ +function showStoredToasts() { + const storedToasts = getStoredToasts(); + if (!storedToasts) return + console.log("Loaded stored notifications to show: ", storedToasts); + for (const toast of storedToasts) { + const toastId = `${toast.time}-toast`; + console.log("Showing stored toast: ", toast, toastId); + const toastElement = bootstrap.Toast.getOrCreateInstance(document.getElementById(toastId)); + toastElement.show(); + } +} + +/** + * Loads all toasts stored in the local storage into the DOM of the webpage + */ +function loadStoredToasts() { + const storedToasts = getStoredToasts(); + if (!storedToasts) return + console.log("Loaded stored notifications: ", storedToasts); + for (const toast of storedToasts) { + createToast(toast.message, { time: toast.time }) + } +} + +/** + * Will update the count of notifications on the bell icon in the navbar + * + * @param {Array} storedToasts An array of stored toasts to be counted and updated in the navbar + */ +function navbarUpdateNotificationBellCount(storedToasts) { + const notificationBellIcon = document.getElementById("navbar-notification-bell"); + var notificationBellCount = document.getElementById("notification-bell-icon-count"); + if (!notificationBellCount) { + notificationBellCount = document.createElement('span'); + notificationBellCount.id = "notification-bell-icon-count"; + notificationBellCount.classList.add('badge'); + notificationBellCount.classList.add('text-bg-secondary'); + notificationBellCount.appendChild(document.createTextNode(storedToasts.length)); + } + else notificationBellCount.innerHTML = storedToasts.length; + + notificationBellIcon.appendChild(notificationBellCount); +} + +/** + * Remove a frequency input from the DOM + * + * @param {string} system The system name to add the frequency to + * @param {string} inputId [OPTIONAL] The ID of input, this can be anything unique to this input. If this is not provided the number of frequencies will be used as the ID + */ +function addFrequencyInput(system, inputId = null) { + if (!inputId) inputId = $(`[id^="${system}_systemFreqRow_"]`).length; + // Create new input + var icon = document.createElement('i'); + icon.classList.add('bi'); + icon.classList.add('bi-x-circle'); + icon.classList.add('text-black'); + + var remove = document.createElement('a'); + remove.classList.add('align-middle'); + remove.classList.add('float-left'); + remove.href = '#' + remove.onclick = () => { removeFrequencyInput(`${system}_systemFreqRow_${inputId}`) } + remove.appendChild(icon); + + var childColRemoveIcon = document.createElement('div'); + childColRemoveIcon.classList.add('col-2'); + childColRemoveIcon.appendChild(remove); + + var input = document.createElement('input'); + input.classList.add('form-control'); + input.id = `${system}_systemFreq_${inputId}`; + input.type = 'text'; + + var childColInput = document.createElement('div'); + childColInput.classList.add('col-10'); + childColInput.appendChild(input); + + var childRow = document.createElement('div'); + childRow.classList.add("row"); + childRow.classList.add("px-1"); + childRow.appendChild(childColInput); + childRow.appendChild(childColRemoveIcon); + + var colParent = document.createElement('div'); + colParent.classList.add("col-md-6"); + colParent.classList.add("mb-1"); + colParent.id = `${system}_systemFreqRow_${inputId}` + colParent.appendChild(childRow); + + document.getElementById(`frequencyRow_${system.replaceAll(" ", "_")}`).appendChild(colParent); +} + +/** + * Add a toast element to the DOM + * + * @param {*} notificationMessage The message of the notification + * @param {Date} param1.time The date object for when the toast was created, blank if creating new + * @param {boolean} param1.showNow Show the toast now or just store it + * @returns + */ +function createToast(notificationMessage, { time = undefined, showNow = false } = {}) { + if (!time) time = new Date(Date.now()); + else time = new Date(Date.parse(time)); + const toastTitle = document.createElement('strong'); + toastTitle.classList.add('me-auto'); + toastTitle.appendChild(document.createTextNode("Server Notification")); + + const toastTime = document.createElement('small'); + toastTime.appendChild(document.createTextNode(time.toLocaleString())); + + const toastClose = document.createElement('button'); + toastClose.type = 'button'; + toastClose.classList.add('btn-close'); + toastClose.ariaLabel = 'Close'; + toastClose.setAttribute('data-bs-dismiss', 'toast'); + toastClose.onclick = () => { removeToastFromStorage(time.toISOString(), notificationMessage); }; + + const toastHeader = document.createElement('div'); + toastHeader.classList.add('toast-header'); + toastHeader.appendChild(toastTitle); + toastHeader.appendChild(toastTime); + toastHeader.appendChild(toastClose); + + const toastMessage = document.createElement('p'); + toastMessage.classList.add("px-2"); + toastMessage.appendChild(document.createTextNode(notificationMessage)); + + const toastBody = document.createElement('div'); + toastBody.classList.add('toast-body'); + toastBody.appendChild(toastMessage); + + const wrapperDiv = document.createElement('div'); + wrapperDiv.classList.add('toast'); + //wrapperDiv.classList.add('position-fixed'); + wrapperDiv.id = `${time.toISOString()}-toast`; + wrapperDiv.role = 'alert'; + wrapperDiv.ariaLive = 'assertive'; + wrapperDiv.ariaAtomic = true; + wrapperDiv.setAttribute('data-bs-delay', "7500"); + wrapperDiv.setAttribute('data-bs-animation', true); + wrapperDiv.appendChild(toastHeader); + wrapperDiv.appendChild(toastMessage); + + document.getElementById("toastZone").appendChild(wrapperDiv); + addToastToStorage(time.toISOString(), notificationMessage); + if (showNow) { + const toastElement = bootstrap.Toast.getOrCreateInstance(document.getElementById(`${time.toISOString()}-toast`)); + toastElement.show(); + } + + return; +} + +function sendNodeHeartbeat(nodeId) { + const Http = new XMLHttpRequest(); + const url = '/client/requestCheckIn' + nodeId; + Http.open("GET", url); + Http.send(); + + Http.onloadend = (e) => { + console.log(Http.responseText) + createToast(Http.responseText, { showNow: true }); + } +} + +function joinServer() { + const preset = document.getElementById("selectRadioPreset").value; + const clientId = document.getElementById("inputDiscordClientId").value; + const channelId = document.getElementById("inputDiscordChannelId").value; + + const reqBody = { + 'preset': preset, + 'clientId': clientId, + 'channelId': channelId + }; + + console.log(reqBody); + + const Http = new XMLHttpRequest(); + const url = '/bot/join'; + Http.open("POST", url); + Http.setRequestHeader("Content-Type", "application/json"); + Http.send(JSON.stringify(reqBody)); + + Http.onloadend = (e) => { + const responseObject = JSON.parse(Http.responseText) + console.log(Http.status); + console.log(responseObject); + createToast(`${responseObject.name} will join shortly`); + location.reload(); + } +} + +function leaveServer() { + const reqBody = {}; + + const Http = new XMLHttpRequest(); + const url = '/bot/leave'; + Http.open("POST", url); + Http.setRequestHeader("Content-Type", "application/json"); + Http.send(JSON.stringify(reqBody)); + + Http.onloadend = (e) => { + const responseObject = Http.responseText; + console.log(Http.status); + console.log(responseObject); + createToast(`${responseObject} is leaving`, { showNow: true }); + } +} + +function saveNodeDetails() { + const nodeName = document.getElementById("inputNodeName").value; + const nodeIp = document.getElementById("inputNodeIp").value; + const nodePort = document.getElementById("inputOrgName").value; + const nodeLocation = document.getElementById("inputNodeLocation").value; + + const reqBody = { + 'name': nodeName, + 'ip': nodeIp, + 'port': nodePort, + 'location': nodeLocation + } + + console.log("Request Body: ", reqBody); + + const Http = new XMLHttpRequest(); + const url = '/client'; + Http.open("PUT", url); + Http.setRequestHeader("Content-Type", "application/json"); + Http.send(JSON.stringify(reqBody)); + + Http.onloadend = (e) => { + const responseObject = JSON.parse(Http.responseText); + console.log(Http.status); + console.log(responseObject); + createToast(`Node Updated!`); + location.reload(); + } +} + +function addNewSystem() { + const nodeId = document.getElementById("nodeId").value; + const systemName = document.getElementById(`New System_systemName`).value; + const systemMode = document.getElementById(`New System_systemMode`).value; + const inputSystemFreqs = $(`[id^="New System_systemFreq_"]`); + let systemFreqs = []; + for (const inputFreq of inputSystemFreqs) { + systemFreqs.push(inputFreq.value); + } + + const reqBody = { + 'systemName': systemName, + 'mode': systemMode, + 'frequencies': systemFreqs + } + if (reqBody.mode == "p25") reqBody.trunkFile = 'none'; + + console.log("Request Body: ", reqBody); + const Http = new XMLHttpRequest(); + const url = "/client/addPreset"; + Http.open("POST", url); + Http.setRequestHeader("Content-Type", "application/json"); + Http.send(JSON.stringify(reqBody)); + + Http.onloadend = (e) => { + const responseObject = Http.responseText + console.log(Http.status); + console.log(responseObject); + createToast(`${systemName} Added!`); + location.reload(); + } +} + +function updateSystem(systemName) { + const nodeId = document.getElementById("nodeId").value; + const systemMode = document.getElementById(`${systemName}_systemMode`).value; + const inputSystemFreqs = $(`[id^="${systemName}_systemFreq_"]`); + let systemFreqs = []; + for (const inputFreq of inputSystemFreqs) { + systemFreqs.push(inputFreq.value); + } + + const reqBody = { + 'systemName': systemName, + 'mode': systemMode, + 'frequencies': systemFreqs + } + if (reqBody.mode == "p25") reqBody.trunkFile = 'none'; + + console.log("Request Body: ", reqBody); + const Http = new XMLHttpRequest(); + const url = "/client/updatePreset"; + Http.open("POST", url); + Http.setRequestHeader("Content-Type", "application/json"); + Http.send(JSON.stringify(reqBody)); + + Http.onloadend = (e) => { + const responseObject = Http.responseText; + console.log(Http.status); + console.log(responseObject); + createToast(`${systemName} Updated!`); + location.reload(); + } +} + +function removeSystem(systemName) { + const nodeId = document.getElementById("nodeId").value; + + const reqBody = { + 'systemName': systemName, + } + + console.log("Request Body: ", reqBody); + const Http = new XMLHttpRequest(); + const url = '/client/removePreset'; + Http.open("POST", url); + Http.setRequestHeader("Content-Type", "application/json"); + Http.send(JSON.stringify(reqBody)); + Http.onloadend = (e) => { + const responseObject = Http.responseText; + console.log(Http.status); + console.log(responseObject); + createToast(`${systemName} Removed!`); + location.reload(); + } +} + +function requestNodeUpdate() { + +} + +function removeFrequencyInput(elementId) { + const element = document.getElementById(elementId); + element.remove(); +} \ No newline at end of file diff --git a/Client/public/stylesheets/style.css b/Client/public/stylesheets/style.css deleted file mode 100644 index 9453385..0000000 --- a/Client/public/stylesheets/style.css +++ /dev/null @@ -1,8 +0,0 @@ -body { - padding: 50px; - font: 14px "Lucida Grande", Helvetica, Arial, sans-serif; -} - -a { - color: #00B7FF; -} diff --git a/Client/routes/bot.js b/Client/routes/bot.js index 805f8a1..3af6afa 100644 --- a/Client/routes/bot.js +++ b/Client/routes/bot.js @@ -15,7 +15,9 @@ router.get('/status', botController.getStatus); * * @param req The request sent from the master * @param req.body.channelId The channel ID to join +* @param req.body.clientId The discord Client ID to use when connecting to the server * @param req.body.presetName The name of the preset to start listening to +* @param req.body.NGThreshold [OPTIONAL] The noisegate threshold, this will default to 50 */ router.post('/join', botController.joinServer); diff --git a/Client/routes/client.js b/Client/routes/client.js index 8e2e8a6..36d6bd5 100644 --- a/Client/routes/client.js +++ b/Client/routes/client.js @@ -2,7 +2,7 @@ const express = require('express'); const router = express.Router(); // Controllers -const { requestCheckIn, getPresets, updatePreset, addNewPreset, removePreset, updateClient } = require("../controllers/clientController"); +const { requestCheckIn, getPresets, updatePreset, addNewPreset, removePreset, updateClient, updateClientConfigWrapper } = require("../controllers/clientController"); /** GET Request a check in from the client * Queue the client to check in with the server @@ -16,6 +16,16 @@ router.get('/requestCheckIn', requestCheckIn); */ router.get('/presets', getPresets); +/** + * PUT An update to the running client config (not radio config) + * @param {number} req.body.id The ID given to the node to update + * @param {string} req.body.name The name of the node + * @param {string} req.body.ip The IP the server can contact the node on + * @param {number} req.body.port The port the server can contact the node on + * @param {string} req.body.location The physical location of the node + */ +router.put('/', updateClientConfigWrapper); + /** POST Update to preset * * @param req The request sent from the master diff --git a/Client/routes/index.js b/Client/routes/index.js index ecca96a..bf776f0 100644 --- a/Client/routes/index.js +++ b/Client/routes/index.js @@ -1,9 +1,11 @@ var express = require('express'); var router = express.Router(); +const { getFullConfig } = require('../utilities/configHandler'); /* GET home page. */ -router.get('/', function(req, res, next) { - res.render('index', { title: 'Express' }); +router.get('/', async function(req, res, next) { + const clientConfig = await getFullConfig(); + res.render('index', { 'node': clientConfig }); }); module.exports = router; diff --git a/Client/utilities/configHandler.js b/Client/utilities/configHandler.js index 0d7750e..1b92e79 100644 --- a/Client/utilities/configHandler.js +++ b/Client/utilities/configHandler.js @@ -2,6 +2,8 @@ const { DebugBuilder } = require("../utilities/debugBuilder.js"); const log = new DebugBuilder("client", "configController"); // Modules +const { nodeObject } = require("./utilities.js"); +const { getPresets } = require("../utilities/updatePresets"); const { readFileSync } = require('fs'); const path = require("path"); require('dotenv').config(); @@ -33,4 +35,8 @@ function getDeviceName(){ log.DEBUG("Device Name: ", DeviceName); return DeviceName; } -exports.getDeviceName = getDeviceID; \ No newline at end of file +exports.getDeviceName = getDeviceID; + +exports.getFullConfig = () => { + return new nodeObject({_id: process.env.CLIENT_ID, _ip: process.env.CLIENT_IP, _name: process.env.CLIENT_NAME, _port: process.env.CLIENT_PORT, _location: process.env.CLIENT_LOCATION, _nearbySystems: getPresets()}); +} \ No newline at end of file diff --git a/Client/utilities/httpRequests.js b/Client/utilities/httpRequests.js index a4b5566..beecbaf 100644 --- a/Client/utilities/httpRequests.js +++ b/Client/utilities/httpRequests.js @@ -9,11 +9,11 @@ const { isJsonString } = require("./utilities.js"); exports.requestOptions = class requestOptions { constructor(path, method, hostname = undefined, headers = undefined, port = undefined) { - if (method === "POST"){ + if (["POST", "PUT"].includes(method)){ log.VERBOSE("Hostname Vars: ", hostname, process.env.SERVER_HOSTNAME, process.env.SERVER_IP); if (hostname) this.hostname = hostname; - if (process.env.SERVER_HOSTNAME) this.hostname = process.env.SERVER_HOSTNAME; - if (process.env.SERVER_IP) this.hostname = process.env.SERVER_IP; + if (!this.hostname && process.env.SERVER_HOSTNAME) this.hostname = process.env.SERVER_HOSTNAME; + if (!this.hostname && process.env.SERVER_IP) this.hostname = process.env.SERVER_IP; if (!this.hostname) throw new Error("No server hostname / IP was given when creating a request"); this.path = path; this.port = port ?? process.env.SERVER_PORT; diff --git a/Client/utilities/updateConfig.js b/Client/utilities/updateConfig.js index e934603..9e90ba8 100644 --- a/Client/utilities/updateConfig.js +++ b/Client/utilities/updateConfig.js @@ -3,6 +3,7 @@ const { DebugBuilder } = require("../utilities/debugBuilder.js"); const log = new DebugBuilder("client", "updateConfig"); // Modules const replace = require('replace-in-file'); +const { getFullConfig } = require("./configHandler.js"); class Options { constructor(key, updatedValue) { @@ -23,6 +24,66 @@ exports.updateId = (updatedId) => { this.updateConfig('CLIENT_ID', updatedId); } +/** + * Wrapper to update any or all keys in the client config + * + * @param {Object} configObject Object with what keys you wish to update (node object format, will be converted) + * @param {number} configObject.id The ID given to the node to update + * @param {string} configObject.name The name of the node + * @param {string} configObject.ip The IP the server can contact the node on + * @param {number} configObject.port The port the server can contact the node on + * @param {string} configObject.location The physical location of the node + * @returns + */ +exports.updateClientConfig = (configObject) => { + const runningConfig = getFullConfig(); + var updatedKeys = [] + const configKeys = Object.keys(configObject); + + if (configKeys.includes("id")) { + if (runningConfig.id != configObject.id) { + this.updateConfig('CLIENT_ID', configObject.id); + updatedKeys.push({'CLIENT_ID': configObject.id}); + process.env.CLIENT_ID = configObject.id; + log.DEBUG("Updated ID to: ", configObject.id); + } + } + if (configKeys.includes("name")) { + if (runningConfig.name != configObject.name) { + this.updateConfig('CLIENT_NAME', configObject.name); + updatedKeys.push({'CLIENT_NAME': configObject.name}); + process.env.CLIENT_NAME = configObject.name; + log.DEBUG("Updated name to: ", configObject.name); + } + } + if (configKeys.includes("ip")) { + if (runningConfig.ip != configObject.ip) { + this.updateConfig('CLIENT_IP', configObject.ip); + updatedKeys.push({'CLIENT_IP': configObject.ip}); + process.env.CLIENT_IP = configObject.ip; + log.DEBUG("Updated ip to: ", configObject.ip); + } + } + if (configKeys.includes("port")) { + if (runningConfig.port != configObject.port) { + this.updateConfig('CLIENT_PORT', configObject.port); + updatedKeys.push({'CLIENT_PORT': configObject.port}); + process.env.CLIENT_PORT = configObject.port; + log.DEBUG("Updated port to: ", configObject.port); + } + } + if (configKeys.includes("location")) { + if (runningConfig.location != configObject.location) { + this.updateConfig('CLIENT_LOCATION', configObject.location); + updatedKeys.push({'CLIENT_LOCATION': configObject.location}); + process.env.CLIENT_LOCATION = configObject.location; + log.DEBUG("Updated location to: ", configObject.location); + } + } + + return updatedKeys; +} + /** * * @param {string} key The config file key to update with the value @@ -37,7 +98,6 @@ exports.updateConfig = function updateConfig(key, value) { }) } - /** * Wrapper to write changes to the file * @param options An instance of the Objects class specified to the key being updated diff --git a/Client/utilities/updatePresets.js b/Client/utilities/updatePresets.js index 1e7bac6..21d6abd 100644 --- a/Client/utilities/updatePresets.js +++ b/Client/utilities/updatePresets.js @@ -17,7 +17,7 @@ function writePresets(presets, callback = undefined) { // Error checking if (err) throw err; log.DEBUG("Write Complete"); - if (callback) callback() + if (callback) callback(); else return }); } @@ -71,8 +71,9 @@ function convertFrequencyToHertz(frequency){ */ exports.getPresets = function getPresets() { const presetDir = path.resolve("./config/radioPresets.json"); - log.DEBUG(`Getting presets from directory: '${presetDir}'`); - return JSON.parse(fs.readFileSync(presetDir)); + log.DEBUG(`Getting presets from directory: '${presetDir}'`); + if (fs.existsSync(presetDir)) return JSON.parse(fs.readFileSync(presetDir)); + else return {}; } /** diff --git a/Client/utilities/utilities.js b/Client/utilities/utilities.js index 8ccad58..78733be 100644 --- a/Client/utilities/utilities.js +++ b/Client/utilities/utilities.js @@ -25,17 +25,15 @@ exports.nodeObject = class nodeObject { * @param {*} param0._ip The IP that the master can contact the node at * @param {*} param0._port The port that the client is listening on * @param {*} param0._location The physical location of the node - * @param {*} param0._online An integer representation of the online status of the bot, ie 0=off, 1=on * @param {*} param0._nearbySystems An object array of nearby systems */ - constructor({ _id = null, _name = null, _ip = null, _port = null, _location = null, _nearbySystems = null, _online = null }) { + constructor({ _id = null, _name = null, _ip = null, _port = null, _location = null, _nearbySystems = null }) { this.id = _id; this.name = _name; this.ip = _ip; this.port = _port; this.location = _location; this.nearbySystems = _nearbySystems; - this.online = _online; } } diff --git a/Client/views/index.ejs b/Client/views/index.ejs index 7b7a1d6..84aa23d 100644 --- a/Client/views/index.ejs +++ b/Client/views/index.ejs @@ -1,11 +1,169 @@ - - - <%= title %> - - - -

<%= title %>

-

Welcome to <%= title %>

- - + + + + + '<%=node.name%>' - Configuration + + + + + + + <%- include('partials/navbar.ejs') %> +
+ + + + +
+
+
+
+
+
+

+ + Node Details + +

+
+
+
+ + Online + +
+
+ + Join Server + + Leave Server + + Check-in + with Node + + Update Node +
+
+
+
+
+ + +
+
+ +
+
+ + +
+
+
+
+ + +
+
+ + +
+
+
+ + +
+

+ Nearby Systems +

+
+
+
+
+
+
+ + + + + + + + + + + <% for(const system in node.nearbySystems){ %> + + + + + + + <% // Update system modal %> + <%- include("partials/modifySystemModal.ejs", {'system': system, 'frequencies' : + node.nearbySystems[system].frequencies, 'mode' : node.nearbySystems[system].mode}) %> + <% } %> + +
System NameFrequenciesProtocol 
+ <%= system %> + + <% if(node.nearbySystems[system].frequencies.length> 1) { %> +
    + <% for(const frequency of node.nearbySystems[system].frequencies) { %> +
  • + <%=frequency%> MHz +
  • + <% } %> +
+ <% } else { const frequency=node.nearbySystems[system].frequencies[0] %> + <%=frequency%> MHz + <% } %> +
+ + <%= node.nearbySystems[system].mode %> + + + "> + + + + + +
+
+
+
+
+
+ + + + +
+
+
+
+ + <% // new System Modal %> + <%- include("partials/modifySystemModal.ejs", {'system': "New System" , 'frequencies' : [], 'mode' : '' }) %> + <% // Join Server Modal %> + <%- include("partials/joinModal.ejs", {'node': node}) %> + + + + + + + + \ No newline at end of file diff --git a/Client/views/partials/joinModal.ejs b/Client/views/partials/joinModal.ejs new file mode 100644 index 0000000..1cc6feb --- /dev/null +++ b/Client/views/partials/joinModal.ejs @@ -0,0 +1,44 @@ + \ No newline at end of file diff --git a/Client/views/partials/modifySystemModal.ejs b/Client/views/partials/modifySystemModal.ejs new file mode 100644 index 0000000..664a755 --- /dev/null +++ b/Client/views/partials/modifySystemModal.ejs @@ -0,0 +1,61 @@ + \ No newline at end of file diff --git a/Client/views/partials/navbar.ejs b/Client/views/partials/navbar.ejs new file mode 100644 index 0000000..b9816fc --- /dev/null +++ b/Client/views/partials/navbar.ejs @@ -0,0 +1,42 @@ + \ No newline at end of file diff --git a/Server/commands/join.js b/Server/commands/join.js index 6dab9a7..5de2aa5 100644 --- a/Server/commands/join.js +++ b/Server/commands/join.js @@ -1,94 +1,13 @@ // Modules const { SlashCommandBuilder } = require('discord.js'); const { DebugBuilder } = require("../utilities/debugBuilder"); -const { getMembersInRole, getAllClientIds, filterAutocompleteValues } = require("../utilities/utils"); -const { requestOptions, sendHttpRequest } = require("../utilities/httpRequests"); -const { getOnlineNodes, updateNodeInfo, addNodeConnection, getConnectionByNodeId, getAllConnections } = require("../utilities/mysqlHandler"); +const { filterAutocompleteValues, filterPresetsAvailable } = require("../utilities/utils"); +const { getOnlineNodes, getAllConnections } = require("../utilities/mysqlHandler"); +const { joinServerWrapper } = require("../controllers/adminController"); // Global Vars const log = new DebugBuilder("server", "join"); -/** - * * This wrapper will check if there is an available node with the requested preset and if so checks for an available client ID to join with - * - * @param {*} presetName The preset name to listen to on the client - * @param {*} channelId The channel ID to join the bot to - * @param {*} connections EITHER A collection of clients that are currently connected OR a single discord client ID (NOT dev portal ID) that should be used to join the server with - * @returns - */ -async function joinServerWrapper(presetName, channelId, connections) { - // Get nodes online - var onlineNodes = await new Promise((recordResolve, recordReject) => { - getOnlineNodes((nodeRows) => { - recordResolve(nodeRows); - }); - }); - - // Check which nodes have the selected preset - onlineNodes = onlineNodes.filter(node => node.presets.includes(presetName)); - log.DEBUG("Filtered Online Nodes: ", onlineNodes); - - // Check if any nodes with this preset are available - var nodesCurrentlyAvailable = []; - for (const node of onlineNodes) { - const currentConnection = await getConnectionByNodeId(node.id); - log.DEBUG("Checking to see if there is a connection for Node: ", node, currentConnection); - if(!currentConnection) nodesCurrentlyAvailable.push(node); - } - log.DEBUG("Nodes Currently Available: ", nodesCurrentlyAvailable); - - // If not, let the user know - if (!nodesCurrentlyAvailable.length > 0) return Error("All nodes with this channel are unavailable, consider swapping one of the currently joined bots."); - - // If so, join with the first node - var availableClientIds = await getAllClientIds(); - log.DEBUG("All clients: ", Object.keys(availableClientIds)); - - var selectedClientId; - if (typeof connections === 'string') { - for (const availableClientId of availableClientIds) { - if (availableClientId.discordId != connections ) selectedClientId = availableClientId; - } - } - else { - log.DEBUG("Open connections: ", connections); - for (const connection of connections) { - log.DEBUG("Used Client ID: ", connection); - availableClientIds = availableClientIds.filter(cid => cid.discordId != connection.clientObject.discordId); - } - - log.DEBUG("Available Client IDs: ", availableClientIds); - - if (!Object.keys(availableClientIds).length > 0) return log.ERROR("All client ID have been used, consider swapping one of the curretly joined bots or adding more Client IDs to the pool.") - selectedClientId = availableClientIds[0]; - } - - const selectedNode = nodesCurrentlyAvailable[0]; - - const reqOptions = new requestOptions("/bot/join", "POST", selectedNode.ip, selectedNode.port); - const postObject = { - "channelId": channelId, - "clientId": selectedClientId.clientId, - "presetName": presetName - }; - log.INFO("Post Object: ", postObject); - sendHttpRequest(reqOptions, JSON.stringify(postObject), async (responseObj) => { - log.VERBOSE("Response Object from node ", selectedNode, responseObj); - if (!responseObj || !responseObj.statusCode == 200) return false; - // Node has connected to discord - // Updating node Object in DB - const updatedNode = await updateNodeInfo(selectedNode); - log.DEBUG("Updated Node: ", updatedNode); - - // Adding a new node connection - const nodeConnection = await addNodeConnection(selectedNode, selectedClientId); - log.DEBUG("Node Connection: ", nodeConnection); - }); - - return selectedClientId; -} -exports.joinServerWrapper = joinServerWrapper; - module.exports = { data: new SlashCommandBuilder() .setName('join') @@ -109,18 +28,7 @@ module.exports = { recordResolve(nodeRows); }); }); - log.DEBUG("Node objects: ", nodeObjects); - var presetsAvailable = []; - for (const nodeObject of nodeObjects) { - log.DEBUG("Node object: ", nodeObject); - presetsAvailable.push.apply(presetsAvailable, nodeObject.presets); - } - - log.DEBUG("All Presets available: ", presetsAvailable); - - // Remove duplicates - options = [...new Set(presetsAvailable)]; - log.DEBUG("DeDuped Presets available: ", options); + const options = await filterPresetsAvailable(nodeObjects); // Filter the results to what the user is entering filterAutocompleteValues(interaction, options); diff --git a/Server/commands/leave.js b/Server/commands/leave.js index c5ad0b2..760c5b0 100644 --- a/Server/commands/leave.js +++ b/Server/commands/leave.js @@ -2,36 +2,12 @@ const { SlashCommandBuilder } = require('discord.js'); const { DebugBuilder } = require("../utilities/debugBuilder"); const { getAllClientIds, getKeyByArrayValue, filterAutocompleteValues } = require("../utilities/utils"); -const { requestOptions, sendHttpRequest } = require("../utilities/httpRequests"); -const { checkNodeConnectionByClientId, removeNodeConnectionByNodeId, getAllConnections } = require('../utilities/mysqlHandler'); +const { getAllConnections } = require('../utilities/mysqlHandler'); +const { leaveServerWrapper } = require('../controllers/adminController'); // Global Vars const log = new DebugBuilder("server", "leave"); -async function leaveServerWrapper(clientIdObject) { - if (!clientIdObject.clientId || !clientIdObject.name) return log.ERROR("Tried to leave server without client ID and/or Name"); - - const node = await checkNodeConnectionByClientId(clientIdObject); - - reqOptions = new requestOptions("/bot/leave", "POST", node.ip, node.port); - - const responseObj = await new Promise((recordResolve, recordReject) => { - sendHttpRequest(reqOptions, JSON.stringify({}), async (responseObj) => { - recordResolve(responseObj); - }); - }); - - log.VERBOSE("Response Object from node ", node, responseObj); - if (!responseObj || !responseObj.statusCode == 202) return false; - // Node has disconnected from discor - // Removing the node connection from the DB - const removedConnection = removeNodeConnectionByNodeId(node.id); - log.DEBUG("Removed Node Connection: ", removedConnection); - - return; -} -exports.leaveServerWrapper = leaveServerWrapper; - module.exports = { data: new SlashCommandBuilder() .setName('leave') @@ -59,7 +35,6 @@ module.exports = { log.DEBUG("Client names: ", clinetIds); const clientDiscordId = getKeyByArrayValue(clinetIds, {'name': botName}); log.DEBUG("Selected bot: ", clinetIds[clientDiscordId]); - // Need to create a table in DB to keep track of what bots have what IDs or an endpoint on the clients to return what ID they are running with await leaveServerWrapper(clinetIds[clientDiscordId]); await interaction.editReply(`**${clinetIds[clientDiscordId].name}** has been disconnected`); // This will reply to the initial interaction diff --git a/Server/controllers/adminController.js b/Server/controllers/adminController.js index 46b6b8c..df6f80c 100644 --- a/Server/controllers/adminController.js +++ b/Server/controllers/adminController.js @@ -4,9 +4,9 @@ require('dotenv').config(); const { DebugBuilder } = require("../utilities/debugBuilder.js"); const log = new DebugBuilder("server", "adminController"); // Utilities -const mysqlHandler = require("../utilities/mysqlHandler"); -const utils = require("../utilities/utils"); -const requests = require("../utilities/httpRequests"); +const { getAllClientIds } = require("../utilities/utils"); +const { getOnlineNodes, updateNodeInfo, addNodeConnection, getConnectionByNodeId, getNodeInfoFromId, checkNodeConnectionByClientId, removeNodeConnectionByNodeId } = require("../utilities/mysqlHandler"); +const { requestOptions, sendHttpRequest } = require("../utilities/httpRequests"); /** Get the presets of all online nodes, can be used for functions * @@ -14,32 +14,15 @@ const requests = require("../utilities/httpRequests"); * @returns {*} A list of the systems online */ async function getPresetsOfOnlineNodes(callback) { - mysqlHandler.getOnlineNodes((onlineNodes) => { - let systems = {}; - onlineNodes.forEach(onlineNode => { - systems[onlineNode.id] = utils.BufferToJson(onlineNode.nearbySystems); - }); - - return callback(systems); + getOnlineNodes((onlineNodes) => { + return callback(onlineNodes); }); } -async function requestNodeListenToPreset(preset, nodeId, callback) { - mysqlHandler.getNodeInfoFromId(nodeId, (nodeObject) =>{ - reqOptions = new requests.requestOptions("/bot/join", "POST", nodeObject.ip, nodeObject.port); - requests.sendHttpRequest(reqOptions, JSON.stringify({ - "channelID": process.env.DEFAULT_VOICE_CHANNEL_ID, - "presetName": preset - }), (responseObject) => { - return callback(responseObject) - }); - }) -} - async function getNodeBotStatus(nodeId, callback) { - mysqlHandler.getNodeInfoFromId(nodeId, (nodeObject) =>{ - reqOptions = new requests.requestOptions("/bot/status", "GET", nodeObject.ip, nodeObject.port, undefined, 5); - requests.sendHttpRequest(reqOptions, JSON.stringify({}), (responseObject) => { + getNodeInfoFromId(nodeId, (nodeObject) =>{ + reqOptions = new requestOptions("/bot/status", "GET", nodeObject.ip, nodeObject.port, undefined, 5); + sendHttpRequest(reqOptions, JSON.stringify({}), (responseObject) => { if (responseObject === false) { // Bot is joined } @@ -51,24 +34,6 @@ async function getNodeBotStatus(nodeId, callback) { }); } -async function requestNodeLeaveServer(nodeId, callback) { - getNodeBotStatus(nodeId, (responseObject) => { - if (responseObject === false) { - // Bot is joined - mysqlHandler.getNodeInfoFromId(nodeId, (nodeObject) =>{ - reqOptions = new requests.requestOptions("/bot/leave", "POST", nodeObject.ip, nodeObject.port); - requests.sendHttpRequest(reqOptions, JSON.stringify({}), (responseObject) => { - return callback(responseObject); - }); - }); - } - else { - // Bot is free - return callback(false); - } - }) -} - /** Return to requests for the presets of all online nodes, cannot be used in functions * @@ -87,43 +52,161 @@ exports.getAvailablePresets = async (req, res) => { * * @param {*} req Express request parameter * @var {*} req.body.preset The preset to join (REQ) - * @var {*} req.body.nodeId The specific node to join (OPT/REQ if more than one node has the preset) + * @var {*} req.body.nodeId The specific node to join (OPT/REQ if more than one node has the preset) + * @var {*} req.body.clientId The ID of the client that we want to join with + * @var {*} req.body.channelId The channel Id of the discord channel to join * @param {*} res Express response parameter */ exports.joinPreset = async (req, res) => { - if (!req.body.preset) return res.status(400).json("No preset specified"); - await getPresetsOfOnlineNodes((systems) => { - const systemsWithSelectedPreset = Object.values(systems).filter(nodePresets => nodePresets.includes(req.body.preset)).length - if (!systemsWithSelectedPreset) return res.status(400).json("No system online with that preset"); - if (systemsWithSelectedPreset > 1) { - if (!req.body.nodeId) return res.status(175).json("Multiple locations with the selected channel, please specify a nodeID (nodeId)") - requestNodeListenToPreset(req.body.preset, req.body.nodeId, (responseObject) => { - if (responseObject === false) return res.status(400).json("Timeout reached"); - return res.sendStatus(responseObject.statusCode); - }); - } - else { - let nodeId; - if (!req.body.nodeId) nodeId = utils.getKeyByArrayValue(systems, req.body.preset); - else nodeId = req.body.nodeId; - requestNodeListenToPreset(req.body.preset, nodeId, (responseObject) => { - if (responseObject === false) return res.status(400).json("Timeout reached"); - return res.sendStatus(responseObject.statusCode); - }); - } - }); + if (!req.body.preset) return res.status(400).json("No preset specified"); + if (!req.body.nodeId) return res.status(400).json("No node ID specified"); + if (!req.body.clientId) return res.status(400).json("No client ID specified"); + if (!req.body.channelId) return res.status(400).json("No channel ID specified"); + + const preset = req.body.preset; + const nodeId = req.body.nodeId; + const clientId = req.body.clientId; + const channelId = req.body.channelId; + + const joinedClient = await joinServerWrapper(preset, channelId, clientId, nodeId); + if (!joinedClient) return res.send(400).json("No joined client"); + return res.status(200).json(joinedClient); } /** Request a node to join the server listening to a specific preset * * @param {*} req Express request parameter * @param {*} res Express response parameter + * @var {*} req.body.nodeId The ID of the node to disconnect */ exports.leaveServer = async (req, res) => { - if (!req.body.nodeId) return res.status(400).json("No nodeID specified"); + if (!req.body.nodeId) return res.status(400).json("No node ID specified"); - requestNodeLeaveServer(req.body.nodeId, (responseObject) => { - if (responseObject === false) return res.status(400).json("Bot not joined to server"); - return res.sendStatus(responseObject.statusCode); - }); -} \ No newline at end of file + const nodeId = req.body.nodeId; + const currentConnection = await getConnectionByNodeId(nodeId); + log.DEBUG("Current Connection for node: ", currentConnection); + + if (!currentConnection) return res.status(400).json("Node is not connected") + + await leaveServerWrapper(currentConnection.clientObject) + + return res.status(200).json(currentConnection.clientObject.name); +} + +/** + * * This wrapper will check if there is an available node with the requested preset and if so checks for an available client ID to join with + * + * @param {*} presetName The preset name to listen to on the client + * @param {*} channelId The channel ID to join the bot to + * @param {*} connections EITHER A collection of clients that are currently connected OR a single discord client ID (NOT dev portal ID) that should be used to join the server with + * @param {number} nodeId [OPTIONAL] The node ID to join with (will join with another node if given node is not available) + * @returns + */ +async function joinServerWrapper(presetName, channelId, connections, nodeId = 0) { + // Get nodes online + var onlineNodes = await new Promise((recordResolve, recordReject) => { + getOnlineNodes((nodeRows) => { + recordResolve(nodeRows); + }); + }); + + // Check which nodes have the selected preset + onlineNodes = onlineNodes.filter(node => node.presets.includes(presetName)); + log.DEBUG("Filtered Online Nodes: ", onlineNodes); + + // Check if any nodes with this preset are available + var nodesCurrentlyAvailable = []; + for (const node of onlineNodes) { + const currentConnection = await getConnectionByNodeId(node.id); + log.DEBUG("Checking to see if there is a connection for Node: ", node, currentConnection); + if(!currentConnection) nodesCurrentlyAvailable.push(node); + } + log.DEBUG("Nodes Currently Available: ", nodesCurrentlyAvailable); + + // If not, let the user know + if (!nodesCurrentlyAvailable.length > 0) return Error("All nodes with this channel are unavailable, consider swapping one of the currently joined bots."); + + // If so, join with the first node + var availableClientIds = await getAllClientIds(); + log.DEBUG("All clients: ", Object.keys(availableClientIds)); + + var selectedClientId; + if (typeof connections === 'string') { + for (const availableClientId of availableClientIds) { + if (availableClientId.discordId != connections ) selectedClientId = availableClientId; + } + } + else { + log.DEBUG("Open connections: ", connections); + for (const connection of connections) { + log.DEBUG("Used Client ID: ", connection); + availableClientIds = availableClientIds.filter(cid => cid.discordId != connection.clientObject.discordId); + } + + log.DEBUG("Available Client IDs: ", availableClientIds); + + if (!Object.keys(availableClientIds).length > 0) return log.ERROR("All client ID have been used, consider swapping one of the curretly joined bots or adding more Client IDs to the pool.") + selectedClientId = availableClientIds[0]; + } + + let selectedNode; + if (nodeId > 0) { + for(const availableNode of nodesCurrentlyAvailable){ + if (availableNode.id == nodeId) selectedNode = availableNode; + } + } + + if (!selectedNode) selectedNode = nodesCurrentlyAvailable[0]; + + const reqOptions = new requestOptions("/bot/join", "POST", selectedNode.ip, selectedNode.port); + const postObject = { + "channelId": channelId, + "clientId": selectedClientId.clientId, + "presetName": presetName + }; + log.INFO("Post Object: ", postObject); + sendHttpRequest(reqOptions, JSON.stringify(postObject), async (responseObj) => { + log.VERBOSE("Response Object from node ", selectedNode, responseObj); + if (!responseObj || !responseObj.statusCode == 200) return false; + // Node has connected to discord + // Updating node Object in DB + const updatedNode = await updateNodeInfo(selectedNode); + log.DEBUG("Updated Node: ", updatedNode); + + // Adding a new node connection + const nodeConnection = await addNodeConnection(selectedNode, selectedClientId); + log.DEBUG("Node Connection: ", nodeConnection); + }); + + return selectedClientId; +} +exports.joinServerWrapper = joinServerWrapper; + +/** + * + * @param {*} clientIdObject The client ID object for the node to leave the server. Either 'clientId'||'name' can be set. + * @returns + */ +async function leaveServerWrapper(clientIdObject) { + if (!clientIdObject.clientId || !clientIdObject.name) return log.ERROR("Tried to leave server without client ID and/or Name"); + + const node = await checkNodeConnectionByClientId(clientIdObject); + + reqOptions = new requestOptions("/bot/leave", "POST", node.ip, node.port); + + const responseObj = await new Promise((recordResolve, recordReject) => { + sendHttpRequest(reqOptions, JSON.stringify({}), async (responseObj) => { + recordResolve(responseObj); + }); + }); + + log.VERBOSE("Response Object from node ", node, responseObj); + if (!responseObj || !responseObj.statusCode == 202) return false; + // Node has disconnected from discor + // Removing the node connection from the DB + const removedConnection = removeNodeConnectionByNodeId(node.id); + log.DEBUG("Removed Node Connection: ", removedConnection); + + return; +} +exports.leaveServerWrapper = leaveServerWrapper; \ No newline at end of file diff --git a/Server/controllers/nodesController.js b/Server/controllers/nodesController.js index 9c9d274..e5f601c 100644 --- a/Server/controllers/nodesController.js +++ b/Server/controllers/nodesController.js @@ -2,18 +2,66 @@ const { DebugBuilder } = require("../utilities/debugBuilder.js"); const log = new DebugBuilder("server", "nodesController"); // Utilities -const {getAllNodes, addNewNode, updateNodeInfo, getNodeInfoFromId, getOnlineNodes } = require("../utilities/mysqlHandler"); +const { getAllNodes, addNewNode, updateNodeInfo, getNodeInfoFromId, getOnlineNodes } = require("../utilities/mysqlHandler"); const utils = require("../utilities/utils"); const { sendHttpRequest, requestOptions } = require("../utilities/httpRequests.js"); const { nodeObject } = require("../utilities/recordHelper.js"); -const { joinServerWrapper } = require("../commands/join"); const refreshInterval = process.env.NODE_MONITOR_REFRESH_INTERVAL ?? 1200000; +const digitalModes = ['p25']; + +/** + * Check in with a singular node, mark it offline if it's offline and + * + * @param {*} node The node Object to check in with + */ +async function checkInWithNode(node) { + const reqOptions = new requestOptions("/client/requestCheckIn", "GET", node.ip, node.port) + sendHttpRequest(reqOptions, "", (responseObj) => { + if (responseObj) { + log.DEBUG("Response from: ", node.name, responseObj); + const onlineNode = new nodeObject({ _online: true, _id: node.id }); + log.DEBUG("Node update object: ", onlineNode); + updateNodeInfo(onlineNode, (sqlResponse) => { + if (!sqlResponse) this.log.ERROR("No response from SQL object"); + + log.DEBUG("Updated node: ", sqlResponse); + return true + }) + } + else { + log.DEBUG("No response from node, assuming it's offline"); + const offlineNode = new nodeObject({ _online: false, _id: node.id }); + log.DEBUG("Offline node update object: ", offlineNode); + updateNodeInfo(offlineNode, (sqlResponse) => { + if (!sqlResponse) this.log.ERROR("No response from SQL object"); + + log.DEBUG("Updated offline node: ", sqlResponse); + return false + }) + } + }) +} +exports.checkInWithNode = checkInWithNode; + +/** + * Check in with all online nodes and mark any nodes that are actually offline +*/ +async function checkInWithOnlineNodes() { + getOnlineNodes((nodes) => { + log.DEBUG("Online Nodes: ", nodes); + for (const node of nodes) { + checkInWithNode(node); + } + return; + }); +} +exports.checkInWithOnlineNodes = checkInWithOnlineNodes; /** * - * @param {*} req - * @param {*} res + * @param {*} req Default express req from router + * @param {*} res Defualt express res from router */ exports.listAllNodes = async (req, res) => { getAllNodes((allNodes) => { @@ -23,7 +71,11 @@ exports.listAllNodes = async (req, res) => { }); } -// Add a new node to the storage +/** + * Add a new node to the storage + * @param {*} req Default express req from router + * @param {*} res Defualt express res from router + */ exports.newNode = async (req, res) => { if (!req.body.name) return res.status(400).json("No name specified for new node"); @@ -40,7 +92,7 @@ exports.newNode = async (req, res) => { addNewNode(newNode, (newNodeObject) => { // Send back a success if the user has been added and the ID for the client to keep track of - res.status(202).json({"nodeId": newNodeObject.id}); + res.status(202).json({ "nodeId": newNodeObject.id }); }) } catch (err) { @@ -53,18 +105,135 @@ exports.newNode = async (req, res) => { } } -// Get the known info for the node specified +/** Get the known info for the node specified + * + * @param {*} req Default express req from router + * @param {*} res Defualt express res from router + */ exports.getNodeInfo = async (req, res) => { - if (!req.query.id) return res.status(400).json("No id specified"); - getNodeInfoFromId(req.query.id, (nodeInfo) => { + if (!req.params.id) return res.status(400).json("No id specified"); + getNodeInfoFromId(req.params.id, (nodeInfo) => { res.status(200).json(nodeInfo); }) } -// Updates the information received from the client based on ID -exports.nodeCheckIn = async (req, res) => { - if (!req.body.id) return res.status(400).json("No id specified"); - getNodeInfoFromId(req.body.id, (nodeInfo) => { +/** Adds a specific system/preset on a given node + * + * @param {*} req Default express req from router + * @param {*} res Defualt express res from router + * @param {*} req.params.nodeId The Node ID to add the preset/system to + * @param {*} req.body.systemName The name of the system to add + * @param {*} req.body.mode The radio mode of the preset + * @param {*} req.body.frequencies The frequencies of the preset + * @param {*} req.body.trunkFile The trunk file to use for digital stations + */ +exports.addNodeSystem = async (req, res) => { + if (!req.params.nodeId) return res.status(400).json("No id specified"); + if (!req.body.systemName) return res.status(400).json("No system specified"); + log.DEBUG("Adding system for node: ", req.params.nodeId, req.body); + getNodeInfoFromId(req.params.nodeId, (node) => { + const reqOptions = new requestOptions("/client/addPreset", "POST", node.ip, node.port); + const reqBody = { + 'systemName': req.body.systemName, + 'mode': req.body.mode, + 'frequencies': req.body.frequencies, + } + if(digitalModes.includes(req.body.mode)) reqBody['trunkFile'] = req.body.trunkFile ?? 'none' + + log.DEBUG("Request body for adding node system: ", reqBody, reqOptions); + sendHttpRequest(reqOptions, JSON.stringify(reqBody), async (responseObj) => { + if(responseObj){ + // Good + log.DEBUG("Response from adding node system: ", reqBody, responseObj); + return res.sendStatus(200) + } else { + // Bad + log.DEBUG("No Response from adding Node system"); + return res.status(400).json("No Response from adding Node, could be offline"); + } + }) + }) +} + +/** Updates a specific system/preset on a given node + * + * @param {*} req Default express req from router + * @param {*} res Defualt express res from router + * @param {*} req.params.nodeId The Node ID to update the preset/system on + * @param {*} req.body.systemName The name of the system to update + * @param {*} req.body.mode The radio mode of the preset to + * @param {*} req.body.frequencies The frequencies of the preset + * @param {*} req.body.trunkFile The trunk file to use for digital stations + */ +exports.updateNodeSystem = async (req, res) => { + if (!req.params.nodeId) return res.status(400).json("No id specified"); + if (!req.body.systemName) return res.status(400).json("No system specified"); + log.DEBUG("Updating system for node: ", req.params.nodeId, req.body); + getNodeInfoFromId(req.params.nodeId, (node) => { + const reqOptions = new requestOptions("/client/updatePreset", "POST", node.ip, node.port); + const reqBody = { + 'systemName': req.body.systemName, + 'mode': req.body.mode, + 'frequencies': req.body.frequencies, + } + if(digitalModes.includes(req.body.mode)) reqBody['trunkFile'] = req.body.trunkFile ?? 'none' + + log.DEBUG("Request body for updating node: ", reqBody, reqOptions); + sendHttpRequest(reqOptions, JSON.stringify(reqBody), async (responseObj) => { + if(responseObj){ + // Good + log.DEBUG("Response from updating node system: ", reqBody, responseObj); + return res.sendStatus(200) + } else { + // Bad + log.DEBUG("No Response from updating Node system"); + return res.status(400).json("No Response from updating Node, could be offline"); + } + }) + }) +} + +/** Deletes a specific system/preset from a given node + * + * @param {*} req Default express req from router + * @param {*} res Defualt express res from router + * @param {*} req.params.nodeId The Node ID to update the preset/system on + * @param {*} req.body.systemName The name of the system to update + */ +exports.removeNodeSystem = async (req, res) => { + if (!req.params.nodeId) return res.status(400).json("No id specified"); + if (!req.body.systemName) return res.status(400).json("No system specified"); + log.DEBUG("Updating system for node: ", req.params.nodeId, req.body); + getNodeInfoFromId(req.params.nodeId, (node) => { + const reqOptions = new requestOptions("/client/removePreset", "POST", node.ip, node.port); + const reqBody = { + 'systemName': req.body.systemName + } + + log.DEBUG("Request body for deleting preset: ", reqBody, reqOptions); + sendHttpRequest(reqOptions, JSON.stringify(reqBody), async (responseObj) => { + if(responseObj){ + // Good + log.DEBUG("Response from deleting preset: ", reqBody, responseObj); + return res.sendStatus(200) + } else { + // Bad + log.DEBUG("No Response from deleting preset"); + return res.status(400).json("No Response from deleting preset, could be offline"); + } + }) + }) +} + + +/** Updates the information received from the client based on ID + * + * @param {*} req Default express req from router + * @param {*} res Defualt express res from router + */ +exports.updateExistingNode = async = (req, res) => { + if (!req.params.nodeId) return res.status(400).json("No id specified"); + getNodeInfoFromId(req.params.nodeId, (nodeInfo) => { let checkInObject = {}; // Convert the online status to a boolean to be worked with log.DEBUG("REQ Body: ", req.body); @@ -74,7 +243,7 @@ exports.nodeCheckIn = async (req, res) => { if (req.body.name && req.body.name != nodeInfo.name) { checkInObject._name = req.body.name; isObjectUpdated = true; - } + } if (req.body.ip && req.body.ip != nodeInfo.ip) { checkInObject._ip = req.body.ip; @@ -102,37 +271,60 @@ exports.nodeCheckIn = async (req, res) => { } // If no changes are made tell the client - if (!isObjectUpdated) return res.status(200).json("No keys updated"); + if (!isObjectUpdated) return res.status(200).json("No keys updated"); - log.INFO("Updating the following keys for ID: ", req.body.id, checkInObject); + log.INFO("Updating the following keys for ID: ", req.params.nodeId, checkInObject); - checkInObject._id = req.body.id; + checkInObject._id = req.params.nodeId; checkInObject = new nodeObject(checkInObject); if (!nodeInfo) { log.WARN("No existing node found with this ID, adding node: ", checkInObject); - addNewNode(checkInObject, (newNode) => { - return res.status(201).json({"updatedKeys": newNode}); + addNewNode(checkInObject, async (newNode) => { + await checkInWithNode(newNode); + return res.status(201).json({ "updatedKeys": newNode }); }); } - else { - updateNodeInfo(checkInObject, () => { - return res.status(202).json({"updatedKeys": checkInObject}); + else { + updateNodeInfo(checkInObject, async () => { + await checkInWithNode(nodeInfo); + return res.status(202).json({ "updatedKeys": checkInObject }); }); - } + } }); } -/** - * Request the node to join the specified server/channel and listen to the specified resource +/** Allows the bots to check in and get any updates from the server * - * @param req.body.clientId The client ID to join discord with (NOT dev portal ID, right click user -> Copy ID) - * @param req.body.channelId The Channel ID to join in Discord - * @param req.body.presetName The preset name to listen to in Discord + * @param {*} req Default express req from router + * @param {*} res Defualt express res from router */ -exports.requestNodeJoinServer = async (req, res) => { - if (!req.body.clientId || !req.body.channelId || !req.body.presetName) return res.status(400).json("Missing information in request, requires clientId, channelId, presetName"); - await joinServerWrapper(req.body.presetName, req.body.channelId, req.body.clientId); +exports.nodeCheckIn = async (req, res) => { + if (!req.params.nodeId) return res.status(400).json("No id specified"); + getNodeInfoFromId(req.params.nodeId, (nodeInfo) => { + if (!nodeInfo) return this.newNode(req, res); + if (!nodeInfo.online) { + nodeInfo.online = true; + updateNodeInfo(nodeInfo, () => { + return res.status(200).json(nodeInfo); + }) + } + else return res.status(200).json(nodeInfo); + }); +} + +/** + * Requests a specific node to check in with the server, if it's online + * + * @param {*} req Default express req from router + * @param {*} res Defualt express res from router + */ +exports.requestNodeCheckIn = async (req, res) => { + if (!req.params.nodeId) return res.status(400).json("No Node ID supplied in request"); + const node = await getNodeInfoFromId(req.params.nodeId); + if (!node) return res.status(400).json("No Node with the ID given"); + await checkInWithNode(node); + if (res) res.sendStatus(200); } /** @@ -143,46 +335,24 @@ exports.nodeMonitorService = class nodeMonitorService { this.log = new DebugBuilder("server", "nodeMonitorService"); } - async start(){ - // Wait for the a portion of the refresh period before checking in with the nodes, so the rest of the bot can start - await new Promise(resolve => setTimeout(resolve, refreshInterval/10)); + /** + * Start the node monitor service in the background + */ + async start() { + // Wait for the a portion of the refresh period before checking in with the nodes, so the rest of the bot can start + await new Promise(resolve => setTimeout(resolve, refreshInterval / 10)); log.INFO("Starting Node Monitor Service"); // Check in before starting the infinite loop - await this.checkInWithOnlineNodes(); - - - while(true){ + await checkInWithOnlineNodes(); + + + while (true) { // Wait for the refresh interval, then wait for the posts to return, then wait a quarter of the refresh interval to make sure everything is cleared up await new Promise(resolve => setTimeout(resolve, refreshInterval)); - await this.checkInWithOnlineNodes(); + await checkInWithOnlineNodes(); await new Promise(resolve => setTimeout(resolve, refreshInterval / 4)); continue; - } - } - - async checkInWithOnlineNodes(){ - getOnlineNodes((nodes) => { - this.log.DEBUG("Online Nodes: ", nodes); - for (const node of nodes) { - const reqOptions = new requestOptions("/client/requestCheckIn", "GET", node.ip, node.port) - sendHttpRequest(reqOptions, "", (responseObj) => { - if (responseObj) { - this.log.DEBUG("Response from: ", node.name, responseObj); - } - else { - this.log.DEBUG("No response from node, assuming it's offline"); - const offlineNode = new nodeObject({ _online: 0, _id: node.id }); - this.log.DEBUG("Offline node update object: ", offlineNode); - updateNodeInfo(offlineNode, (sqlResponse) => { - if (!sqlResponse) this.log.ERROR("No response from SQL object"); - - this.log.DEBUG("Updated offline node: ", sqlResponse); - }) - } - }) - } - return; - }); + } } } diff --git a/Server/public/res/css/main.css b/Server/public/res/css/main.css new file mode 100644 index 0000000..a0aeba6 --- /dev/null +++ b/Server/public/res/css/main.css @@ -0,0 +1,184 @@ +.node-card { + position: relative; + display: flex; + flex-direction: column; + min-width: 0; + word-wrap: break-word; + background-color: #fff; + background-clip: border-box; + border: 1px solid #eff0f2; + border-radius: 1rem; + margin-bottom: 24px; + box-shadow: 0 2px 3px #e4e8f0; +} + +.avatar-md { + height: 4rem; + width: 4rem; +} + +.rounded-circle { + border-radius: 50% !important; +} + +.img-thumbnail { + padding: 0.25rem; + background-color: #f1f3f7; + border: 1px solid #eff0f2; + border-radius: 0.75rem; +} + +.avatar-title { + align-items: center; + background-color: #3b76e1; + color: #fff; + display: flex; + font-weight: 500; + height: 100%; + justify-content: center; + width: 100%; +} + +.bg-soft-primary { + background-color: rgba(59, 118, 225, .25) !important; +} + +a { + text-decoration: none !important; +} + +.badge-soft-danger { + color: #f56e6e !important; + background-color: rgba(245, 110, 110, .1); +} + +.badge-soft-success { + color: #63ad6f !important; + background-color: rgba(99, 173, 111, .1); +} + +.mb-0 { + margin-bottom: 0 !important; +} + +.badge { + display: inline-block; + padding: 0.25em 0.6em; + font-size: 75%; + font-weight: 500; + line-height: 1; + color: #fff; + text-align: center; + white-space: nowrap; + vertical-align: baseline; + border-radius: 0.75rem; +} + +/* Info Card Section */ +.info-card { + background-color: #fff; + border-radius: 10px; + border: none; + position: relative; + margin-bottom: 30px; + box-shadow: 0 0.46875rem 2.1875rem rgba(90, 97, 105, 0.1), 0 0.9375rem 1.40625rem rgba(90, 97, 105, 0.1), 0 0.25rem 0.53125rem rgba(90, 97, 105, 0.12), 0 0.125rem 0.1875rem rgba(90, 97, 105, 0.1); + min-height: 85%; +} + +.info-card .card-statistic .card-icon-large .bi { + font-size: 110px; +} + +.info-card .card-statistic .card-icon { + text-align: center; + line-height: 50px; + margin-left: 15px; + color: #000; + position: absolute; + right: -5px; + top: 20px; + opacity: 0.1; +} + +/* Info Card Background Colors */ + +.l-bg-cherry { + background: linear-gradient(to right, #493240, #f09) !important; + color: #fff; +} + +.l-bg-blue-dark { + background: linear-gradient(to right, #373b44, #4286f4) !important; + color: #fff; +} + +.l-bg-green-dark { + background: linear-gradient(to right, #0a504a, #38ef7d) !important; + color: #fff; +} + +.l-bg-orange-dark { + background: linear-gradient(to right, #a86008, #ffba56) !important; + color: #fff; +} + +.l-bg-cyan { + background: linear-gradient(135deg, #289cf5, #84c0ec) !important; + color: #fff; +} + +.l-bg-green { + background: linear-gradient(135deg, #23bdb8 0%, #43e794 100%) !important; + color: #fff; +} + +.l-bg-orange { + background: linear-gradient(to right, #f9900e, #ffba56) !important; + color: #fff; +} + +/* Global Section */ +.sidebar-container { + min-height: 95vh; +} + +.sidebar { + position: fixed; + top: 5vh; + bottom: 0; + left: 0; + } + +/* User table section */ + +.label { + border-radius: 3px; + font-size: 1.1em; + font-weight: 600; +} + +.user-list tbody td .user-subhead { + font-size: 1em; + font-style: italic; +} + +.table thead tr th { + text-transform: uppercase; + font-size: 0.875em; +} + +.table thead tr th { + border-bottom: 2px solid #e7ebee; +} + +.table tbody tr td:first-child { + font-size: 1.125em; + font-weight: 300; +} + +.table tbody tr td { + font-size: 0.875em; + vertical-align: middle; + border-top: 1px solid #e7ebee; + padding: 12px 8px; +} diff --git a/Server/public/res/js/node.js b/Server/public/res/js/node.js new file mode 100644 index 0000000..2fe1e6a --- /dev/null +++ b/Server/public/res/js/node.js @@ -0,0 +1,407 @@ +$(document).ready(async () => { + console.log("Loading stored notifications..."); + await loadStoredToasts(); + console.log("Showing stored notifications..."); + await showStoredToasts(); +}); + +/** + * Gets all toasts stored in local storage + * + * @returns {Object} Object of toasts in storage + */ +function getStoredToasts() { + if (localStorage.getItem("toasts")) { + const storedToasts = JSON.parse(localStorage.getItem("toasts")); + console.log("LOADED STORED TOASTS: ", storedToasts); + navbarUpdateNotificationBellCount(storedToasts); + return storedToasts; + } + else return false +} + +/** + * Adds a toast to storage, will not allow duplicates + * + * @param {Date} time The date object from when the toast was created + * @param {*} message The message of the toast + */ +function addToastToStorage(time, message) { + var toasts = [{ 'time': time, 'message': message }] + var storedToasts = getStoredToasts(); + console.log("Adding new notification to storage: ", toasts); + if (storedToasts) { + toasts = toasts.concat(storedToasts); + console.log("Combined new and stored notifications: ", toasts); + toasts = toasts.filter((value, index, self) => + index === self.findIndex((t) => ( + t.time === value.time && t.message === value.message + )) + ) + } + console.log("Deduped stored notifications: ", toasts); + localStorage.setItem("toasts", JSON.stringify(toasts)); + navbarUpdateNotificationBellCount(toasts); +} + +/** + * Removes a toast from the local storage + * + * @param {Date} time The date object from when the toast was created + * @param {*} message The message of the toast + */ +function removeToastFromStorage(time, message) { + const toastToRemove = { 'time': time, 'message': message } + console.log("Toast to remove: ", toastToRemove); + var toasts = getStoredToasts(); + console.log("Stored toasts: ", toasts); + if (toasts.indexOf(toastToRemove)) toasts.splice(toasts.indexOf(toastToRemove) - 1, 1) + console.log("Toasts with selected toast removed: ", toasts); + localStorage.setItem("toasts", JSON.stringify(toasts)); + navbarUpdateNotificationBellCount(toasts); +} + +/** + * Shows all stored toasts + */ +function showStoredToasts() { + const storedToasts = getStoredToasts(); + if (!storedToasts) return + console.log("Loaded stored notifications to show: ", storedToasts); + for (const toast of storedToasts) { + const toastId = `${toast.time}-toast`; + console.log("Showing stored toast: ", toast, toastId); + const toastElement = bootstrap.Toast.getOrCreateInstance(document.getElementById(toastId)); + toastElement.show(); + } +} + +/** + * Loads all toasts stored in the local storage into the DOM of the webpage + */ +function loadStoredToasts() { + const storedToasts = getStoredToasts(); + if (!storedToasts) return + console.log("Loaded stored notifications: ", storedToasts); + for (const toast of storedToasts) { + createToast(toast.message, { time: toast.time }) + } +} + +/** + * Will update the count of notifications on the bell icon in the navbar + * + * @param {Array} storedToasts An array of stored toasts to be counted and updated in the navbar + */ +function navbarUpdateNotificationBellCount(storedToasts) { + const notificationBellIcon = document.getElementById("navbar-notification-bell"); + var notificationBellCount = document.getElementById("notification-bell-icon-count"); + if (!notificationBellCount) { + notificationBellCount = document.createElement('span'); + notificationBellCount.id = "notification-bell-icon-count"; + notificationBellCount.classList.add('badge'); + notificationBellCount.classList.add('text-bg-secondary'); + notificationBellCount.appendChild(document.createTextNode(storedToasts.length)); + } + else notificationBellCount.innerHTML = storedToasts.length; + + notificationBellIcon.appendChild(notificationBellCount); +} + +/** + * Remove a frequency input from the DOM + * + * @param {string} system The system name to add the frequency to + * @param {string} inputId [OPTIONAL] The ID of input, this can be anything unique to this input. If this is not provided the number of frequencies will be used as the ID + */ +function addFrequencyInput(system, inputId = null) { + if (!inputId) inputId = $(`[id^="${system}_systemFreqRow_"]`).length; + // Create new input + var icon = document.createElement('i'); + icon.classList.add('bi'); + icon.classList.add('bi-x-circle'); + icon.classList.add('text-black'); + + var remove = document.createElement('a'); + remove.classList.add('align-middle'); + remove.classList.add('float-left'); + remove.href = '#' + remove.onclick = () => { removeFrequencyInput(`${system}_systemFreqRow_${inputId}`) } + remove.appendChild(icon); + + var childColRemoveIcon = document.createElement('div'); + childColRemoveIcon.classList.add('col-2'); + childColRemoveIcon.appendChild(remove); + + var input = document.createElement('input'); + input.classList.add('form-control'); + input.id = `${system}_systemFreq_${inputId}`; + input.type = 'text'; + + var childColInput = document.createElement('div'); + childColInput.classList.add('col-10'); + childColInput.appendChild(input); + + var childRow = document.createElement('div'); + childRow.classList.add("row"); + childRow.classList.add("px-1"); + childRow.appendChild(childColInput); + childRow.appendChild(childColRemoveIcon); + + var colParent = document.createElement('div'); + colParent.classList.add("col-md-6"); + colParent.classList.add("mb-1"); + colParent.id = `${system}_systemFreqRow_${inputId}` + colParent.appendChild(childRow); + + document.getElementById(`frequencyRow_${system.replaceAll(" ", "_")}`).appendChild(colParent); +} + +/** + * Add a toast element to the DOM + * + * @param {*} notificationMessage The message of the notification + * @param {Date} param1.time The date object for when the toast was created, blank if creating new + * @param {boolean} param1.showNow Show the toast now or just store it + * @returns + */ +function createToast(notificationMessage, { time = undefined, showNow = false } = {}) { + if (!time) time = new Date(Date.now()); + else time = new Date(Date.parse(time)); + const toastTitle = document.createElement('strong'); + toastTitle.classList.add('me-auto'); + toastTitle.appendChild(document.createTextNode("Server Notification")); + + const toastTime = document.createElement('small'); + toastTime.appendChild(document.createTextNode(time.toLocaleString())); + + const toastClose = document.createElement('button'); + toastClose.type = 'button'; + toastClose.classList.add('btn-close'); + toastClose.ariaLabel = 'Close'; + toastClose.setAttribute('data-bs-dismiss', 'toast'); + toastClose.onclick = () => { removeToastFromStorage(time.toISOString(), notificationMessage); }; + + const toastHeader = document.createElement('div'); + toastHeader.classList.add('toast-header'); + toastHeader.appendChild(toastTitle); + toastHeader.appendChild(toastTime); + toastHeader.appendChild(toastClose); + + const toastMessage = document.createElement('p'); + toastMessage.classList.add("px-2"); + toastMessage.appendChild(document.createTextNode(notificationMessage)); + + const toastBody = document.createElement('div'); + toastBody.classList.add('toast-body'); + toastBody.appendChild(toastMessage); + + const wrapperDiv = document.createElement('div'); + wrapperDiv.classList.add('toast'); + //wrapperDiv.classList.add('position-fixed'); + wrapperDiv.id = `${time.toISOString()}-toast`; + wrapperDiv.role = 'alert'; + wrapperDiv.ariaLive = 'assertive'; + wrapperDiv.ariaAtomic = true; + wrapperDiv.setAttribute('data-bs-delay', "7500"); + wrapperDiv.setAttribute('data-bs-animation', true); + wrapperDiv.appendChild(toastHeader); + wrapperDiv.appendChild(toastMessage); + + document.getElementById("toastZone").appendChild(wrapperDiv); + addToastToStorage(time.toISOString(), notificationMessage); + if (showNow) { + const toastElement = bootstrap.Toast.getOrCreateInstance(document.getElementById(`${time.toISOString()}-toast`)); + toastElement.show(); + } + + return; +} + +function sendNodeHeartbeat(nodeId) { + const Http = new XMLHttpRequest(); + const url = '/nodes/nodeCheckIn/' + nodeId; + Http.open("GET", url); + Http.send(); + + Http.onloadend = (e) => { + console.log(Http.responseText) + createToast(Http.responseText, { showNow: true }); + } +} + +function joinServer() { + const preset = document.getElementById("selectRadioPreset").value; + const nodeId = document.getElementById("nodeId").value; + const clientId = document.getElementById("inputDiscordClientId").value; + const channelId = document.getElementById("inputDiscordChannelId").value; + + const reqBody = { + 'preset': preset, + 'nodeId': nodeId, + 'clientId': clientId, + 'channelId': channelId + }; + + console.log(reqBody); + + const Http = new XMLHttpRequest(); + const url = '/admin/join'; + Http.open("POST", url); + Http.setRequestHeader("Content-Type", "application/json"); + Http.send(JSON.stringify(reqBody)); + + Http.onloadend = (e) => { + const responseObject = JSON.parse(Http.responseText) + console.log(Http.status); + console.log(responseObject); + createToast(`${responseObject.name} will join shortly`, { showNow: true }); + location.reload(); + } +} + +function leaveServer() { + const nodeId = document.getElementById("nodeId").value; + const reqBody = { + 'nodeId': nodeId + }; + + const Http = new XMLHttpRequest(); + const url = '/admin/leave'; + Http.open("POST", url); + Http.setRequestHeader("Content-Type", "application/json"); + Http.send(JSON.stringify(reqBody)); + + Http.onloadend = (e) => { + const responseObject = JSON.parse(Http.responseText) + console.log(Http.status); + console.log(responseObject); + createToast(`${responseObject} is leaving`, { showNow: true }); + } +} + +function saveNodeDetails() { + const nodeId = document.getElementById("nodeId").value; + const nodeName = document.getElementById("inputNodeName").value; + const nodeIp = document.getElementById("inputNodeIp").value; + const nodePort = document.getElementById("inputOrgName").value; + const nodeLocation = document.getElementById("inputNodeLocation").value; + + const reqBody = { + 'id': nodeId, + 'name': nodeName, + 'ip': nodeIp, + 'port': nodePort, + 'location': nodeLocation + } + + console.log("Request Body: ", reqBody); + + const Http = new XMLHttpRequest(); + const url = '/nodes/' + nodeId; + Http.open("PUT", url); + Http.setRequestHeader("Content-Type", "application/json"); + Http.send(JSON.stringify(reqBody)); + + Http.onloadend = (e) => { + const responseObject = JSON.parse(Http.responseText); + console.log(Http.status); + console.log(responseObject); + createToast(`Node Updated!`); + location.reload(); + } +} + +function addNewSystem() { + const nodeId = document.getElementById("nodeId").value; + const systemName = document.getElementById(`New System_systemName`).value; + const systemMode = document.getElementById(`New System_systemMode`).value; + const inputSystemFreqs = $(`[id^="New System_systemFreq_"]`); + let systemFreqs = []; + for (const inputFreq of inputSystemFreqs) { + systemFreqs.push(inputFreq.value); + } + + const reqBody = { + 'systemName': systemName, + 'mode': systemMode, + 'frequencies': systemFreqs + } + + console.log("Request Body: ", reqBody); + const Http = new XMLHttpRequest(); + const url = '/nodes/' + nodeId + "/systems"; + Http.open("POST", url); + Http.setRequestHeader("Content-Type", "application/json"); + Http.send(JSON.stringify(reqBody)); + + Http.onloadend = (e) => { + const responseObject = Http.responseText + console.log(Http.status); + console.log(responseObject); + createToast(`${systemName} Added!`); + location.reload(); + } +} + +function updateSystem(systemName) { + const nodeId = document.getElementById("nodeId").value; + const systemMode = document.getElementById(`${systemName}_systemMode`).value; + const inputSystemFreqs = $(`[id^="${systemName}_systemFreq_"]`); + let systemFreqs = []; + for (const inputFreq of inputSystemFreqs) { + systemFreqs.push(inputFreq.value); + } + + const reqBody = { + 'systemName': systemName, + 'mode': systemMode, + 'frequencies': systemFreqs + } + + console.log("Request Body: ", reqBody); + const Http = new XMLHttpRequest(); + const url = '/nodes/' + nodeId + "/systems"; + Http.open("PUT", url); + Http.setRequestHeader("Content-Type", "application/json"); + Http.send(JSON.stringify(reqBody)); + + Http.onloadend = (e) => { + const responseObject = Http.responseText; + console.log(Http.status); + console.log(responseObject); + createToast(`${systemName} Updated!`); + location.reload(); + } +} + +function removeSystem(systemName) { + const nodeId = document.getElementById("nodeId").value; + + const reqBody = { + 'systemName': systemName, + } + + console.log("Request Body: ", reqBody); + const Http = new XMLHttpRequest(); + const url = '/nodes/' + nodeId + "/systems"; + Http.open("DELETE", url); + Http.setRequestHeader("Content-Type", "application/json"); + Http.send(JSON.stringify(reqBody)); + Http.onloadend = (e) => { + const responseObject = Http.responseText; + console.log(Http.status); + console.log(responseObject); + createToast(`${systemName} Removed!`); + location.reload(); + } +} + +function requestNodeUpdate() { + +} + +function removeFrequencyInput(elementId) { + const element = document.getElementById(elementId); + element.remove(); +} \ No newline at end of file diff --git a/Server/routes/index.js b/Server/routes/index.js index a188406..87c0a60 100644 --- a/Server/routes/index.js +++ b/Server/routes/index.js @@ -1,31 +1,45 @@ -const libCore = require("../libCore"); var express = require('express'); var router = express.Router(); +const { getAllNodes, getNodeInfoFromId, getAllConnections } = require("../utilities/mysqlHandler"); +const { filterPresetsAvailable } = require("../utilities/utils"); + /* GET home page. */ -router.get('/', (req, res) => { - var sources = libCore.getSources(); - //res.render('index', { "sources": sources }); - - var htmlOutput = ""; - - sources.forEach(source => { - htmlOutput += ` -
- -
Title: ${source.title}
-
Link: ${source.link}
-
category: ${source.category}
- -
-
-
- -
- - ` +router.get('/', async (req, res) => { + var nodes = await new Promise((recordResolve, recordReject) => { + getAllNodes((nodeRows) => { + recordResolve(nodeRows); + }); }); - res.send(htmlOutput); + var connections = await getAllConnections(); + var presets = await new Promise((recordResolve, recordReject) => { + getAllNodes((nodeRows) => { + recordResolve(filterPresetsAvailable(nodeRows)); + }); + }); + + //var sources = libCore.getSources(); + return res.render('index', { 'page': 'index', 'nodes': nodes, 'connections': connections, 'presets': presets }); +}); + +/* GET node controller page. */ +router.get('/controller', async (req, res) => { + var nodes = await new Promise((recordResolve, recordReject) => { + getAllNodes((nodeRows) => { + recordResolve(nodeRows); + }); + }); + + //var sources = libCore.getSources(); + return res.render('controller', { 'nodes': nodes, 'page': 'controller' }); +}); + +/* GET individual node page. */ +router.get('/node/:id', async (req, res) => { + var node = await getNodeInfoFromId(req.params.id); + + //var sources = libCore.getSources(); + return res.render('node', { 'node': node, 'page': 'node' }); }); module.exports = router; diff --git a/Server/routes/nodes.js b/Server/routes/nodes.js index b90e8db..a1fef18 100644 --- a/Server/routes/nodes.js +++ b/Server/routes/nodes.js @@ -5,6 +5,22 @@ const nodesController = require('../controllers/nodesController'); /* GET nodes the server knows */ router.get('/', nodesController.listAllNodes); +// TODO Need to authenticate this request +/* GET the information the server has on a particular node */ +router.get('/:nodeId', nodesController.getNodeInfo); + +// Update an existing node +router.put('/:nodeId', nodesController.updateExistingNode); + +// Add a system to an existing node +router.post('/:nodeId/systems', nodesController.addNodeSystem); + +// Update a system on an existing node +router.put('/:nodeId/systems', nodesController.updateNodeSystem); + +// Delete a system from an existing node +router.delete('/:nodeId/systems', nodesController.removeNodeSystem); + // TODO Need to authenticate this request /* POST a new node to the server * @@ -21,15 +37,10 @@ router.get('/', nodesController.listAllNodes); router.post('/newNode', nodesController.newNode); // TODO Need to authenticate this request -/* GET the information the server has on a particular node */ -router.get('/nodeInfo', nodesController.getNodeInfo); +// Client checkin with the server to update client information +router.post('/nodeCheckIn/:nodeId', nodesController.nodeCheckIn); -// TODO Need to authenticate this request -// Client checkin with the server to update information -router.post('/nodeCheckIn', nodesController.nodeCheckIn); - -// TODO Need to authenticate this request -// Request a particular client to join a particular channel listening to a particular preset -router.post('/joinServer', nodesController.requestNodeJoinServer); +// Request a node to check in with the server +router.get('/nodeCheckIn/:nodeId', nodesController.requestNodeCheckIn); module.exports = router; diff --git a/Server/utilities/recordHelper.js b/Server/utilities/recordHelper.js index 5464374..2b68db3 100644 --- a/Server/utilities/recordHelper.js +++ b/Server/utilities/recordHelper.js @@ -119,7 +119,7 @@ class nodeObject { this.port = _port; this.location = _location; this.nearbySystems = _nearbySystems; - if (this.nearbySystems) this.presets = Object.keys(_nearbySystems); + if (this.nearbySystems) this.presets = Object.keys(_nearbySystems); this.online = _online; } } diff --git a/Server/utilities/utils.js b/Server/utilities/utils.js index 2acef73..5dc3737 100644 --- a/Server/utilities/utils.js +++ b/Server/utilities/utils.js @@ -23,7 +23,7 @@ exports.BufferToJson = (buffer) => { * @returns {string} The sanitized preset name to be used elsewhere */ exports.SanitizePresetName = (presetName) => { - return String(presetName).toLowerCase().replace(/[\W_]+/g,"-") + return String(presetName).toLowerCase().replace(/[\W_]+/g, "-") } /** @@ -32,16 +32,16 @@ exports.SanitizePresetName = (presetName) => { * @param interaction Discord interaction object * @param param0.roleName {OPTIONAL} The role name to check the members in; Defaults to 'Bots' */ -exports.getMembersInRole = async (interaction, roleName = "Bots" ) => { +exports.getMembersInRole = async (interaction, roleName = "Bots") => { log.DEBUG("Fetching all members"); var guild = await interaction.client.guilds.fetch({ guild: interaction.guild.id, cache: false }); //cache all members in the server - await guild.members.fetch({cache: false}); - await guild.roles.fetch({cache: false}); + await guild.members.fetch({ cache: false }); + await guild.roles.fetch({ cache: false }); log.VERBOSE("Guild: ", guild); const role = await guild.roles.cache.find(role => role.name === roleName); //the role to check log.DEBUG("Role to check members from: ", role); log.DEBUG("Members of role: ", role.members); - + // This is not working, can't get the status of the users, rest of join is untested const onlineMembers = await role.members.filter(member => member.voice.channel !== null); const offlineMembers = await role.members.filter(member => member.voice.channel === null); @@ -50,8 +50,8 @@ exports.getMembersInRole = async (interaction, roleName = "Bots" ) => { log.VERBOSE("All members: ", allMembers, onlineMembers, offlineMembers) return { - 'online': onlineMembers, - 'offline': offlineMembers, + 'online': onlineMembers, + 'offline': offlineMembers, 'all': allMembers } } @@ -64,9 +64,9 @@ exports.getMembersInRole = async (interaction, roleName = "Bots" ) => { */ exports.getKeyByArrayValue = (object, value) => { if (typeof value == "string") return Object.keys(object).find(key => object[key].includes(value)); - const valueKey = Object.keys(value)[0]; + const valueKey = Object.keys(value)[0]; return Object.keys(object).find(key => (object[key][valueKey] == value[valueKey])); - } +} /** * Check to see if the input is a valid JSON string @@ -91,7 +91,7 @@ exports.isJsonString = (str) => { exports.getAllClientIds = () => { const jsonClientIds = JSON.parse(readFileSync(path.resolve(__dirname, '../clientIds.json'))); var clientObjects = []; - for (const jsonClientId of Object.keys(jsonClientIds)){ + for (const jsonClientId of Object.keys(jsonClientIds)) { clientObjects.push(new clientObject({ _discord_id: jsonClientId, _name: jsonClientIds[jsonClientId].name, @@ -110,7 +110,7 @@ exports.getAllClientIds = () => { exports.getClientObjectByClientID = (clientId) => { const clientObjects = this.getAllClientIds(); log.DEBUG("All client IDs: ", clientObjects); - for (const clientObject of clientObjects){ + for (const clientObject of clientObjects) { if (clientObject.clientId == clientId) { log.DEBUG("Found client ID from given ID: ", clientObject); return clientObject @@ -119,7 +119,12 @@ exports.getClientObjectByClientID = (clientId) => { return undefined } - +/** + * Wrapper to filter auto complete + * + * @param {*} interaction + * @param {*} options + */ exports.filterAutocompleteValues = async (interaction, options) => { // Get the command used const command = interaction.command; @@ -133,4 +138,26 @@ exports.filterAutocompleteValues = async (interaction, options) => { await interaction.respond( filtered.map(option => ({ name: option, value: option })), ); +} + +/** + * Filter an array of nodeObjects to get all unique presets within + * + * @param {Array} nodeObjects An array of nodeObjects to get the presets from + * @returns {Array} Presets available from given nodeObjects + */ +exports.filterPresetsAvailable = async (nodeObjects) => { + log.DEBUG("Node objects: ", nodeObjects); + var presetsAvailable = []; + for (const nodeObject of nodeObjects) { + log.DEBUG("Node object: ", nodeObject); + presetsAvailable.push.apply(presetsAvailable, nodeObject.presets); + } + + log.DEBUG("All Presets available: ", presetsAvailable); + + // Remove duplicates + presetsAvailable = [...new Set(presetsAvailable)]; + log.DEBUG("DeDuped Presets available: ", presetsAvailable); + return presetsAvailable; } \ No newline at end of file diff --git a/Server/views/controller.ejs b/Server/views/controller.ejs new file mode 100644 index 0000000..6831d79 --- /dev/null +++ b/Server/views/controller.ejs @@ -0,0 +1,11 @@ +<%- include('partials/htmlHead.ejs') %> +
+
+ <% for(const node of nodes) {%> + <%- include('partials/nodeCard.ejs', {'node': node}) %> + <% } %> +
+
+<%- include('partials/bodyEnd.ejs') %> + +<%- include('partials/htmlFooter.ejs') %> \ No newline at end of file diff --git a/Server/views/index.ejs b/Server/views/index.ejs index 7b7a1d6..4eb1c60 100644 --- a/Server/views/index.ejs +++ b/Server/views/index.ejs @@ -1,11 +1,64 @@ - - - - <%= title %> - - - -

<%= title %>

-

Welcome to <%= title %>

- - +<%- include('partials/htmlHead.ejs', {'page': page}) %> +
+
+ <%- include('partials/valueChip.ejs', { + 'title': 'Nodes in the Network', + 'bgColor': "orange-dark", + 'value': nodes.length, + 'progressPercent': false, + 'icon': 'server' + }) %> + + <%- include('partials/valueChip.ejs', { + 'title': 'Nodes Online', + 'bgColor': "green", + 'value': nodes.filter(node => node.online).length, + 'progressPercent': false, + 'icon': 'cpu-fill' + }) %> + + <%- include('partials/valueChip.ejs', { + 'title': 'Nodes with Discord Connections', + 'bgColor': "blue-dark", + 'value': connections.length, + 'progressPercent': false, + 'icon': 'gear-wide-connected' + }) %> + +
+ +
+

Current Connections

+
+ +
+ +
+ <% for(const conn of connections) { %> + <%- include('partials/connectionCard.ejs', {'connection': conn}) %> + <%}%> +
+ +
+

Online Nodes

+
+ +
+ +
+ <% for(const node of nodes.filter(node => node.online)) { %> + <%- include('partials/nodeCard.ejs', {'node': node}) %> + <%}%> +
+
+

Offline Nodes

+
+
+
+ <% for(const node of nodes.filter(node => node.online == false)) { %> + <%- include('partials/nodeCard.ejs', {'node': node}) %> + <%}%> +
+
+<%- include('partials/bodyEnd.ejs') %> +<%- include('partials/htmlFooter.ejs') %> \ No newline at end of file diff --git a/Server/views/node.ejs b/Server/views/node.ejs new file mode 100644 index 0000000..4d8c5d7 --- /dev/null +++ b/Server/views/node.ejs @@ -0,0 +1,140 @@ +<%- include('partials/htmlHead.ejs') %> +
+
+
+

+ + Node Details + +

+
+
+
+ + <% if(node.online){%> Online + <% } else {%> Offline + <% } %> +
+
+ + Join Server + + Leave Server + + Check-in with Node + + Update Node +
+
+
+
+
+ + +
+
+ +
+
+ + +
+
+
+
+ + +
+
+ + +
+
+
+ + +
+

+ Nearby Systems +

+
+
+
+
+
+
+ + + + + + + + + + + <% for(const system in node.nearbySystems){ %> + + + + + + + <% // Update system modal %> + <%- include("partials/modifySystemModal.ejs", {'system': system, 'frequencies': node.nearbySystems[system].frequencies, 'mode': node.nearbySystems[system].mode}) %> + <% } %> + +
System NameFrequenciesProtocol 
+ <%= system %> + + <% if(node.nearbySystems[system].frequencies.length> 1) { %> +
    + <% for(const frequency of + node.nearbySystems[system].frequencies) { %> +
  • + <%=frequency%> MHz +
  • + <% } %> +
+ <% } else { const + frequency=node.nearbySystems[system].frequencies[0] + %> + <%=frequency%> MHz + <% } %> +
+ + <%= node.nearbySystems[system].mode %> + + + "> + + + + + +
+
+
+
+
+
+ + + + +
+
+
+
+ + <% // new System Modal %> + <%- include("partials/modifySystemModal.ejs", {'system': "New System", 'frequencies': [], 'mode': ''}) %> + <% // Join Server Modal %> + <%- include("partials/joinModal.ejs", {'node': node}) %> +<%- include('partials/bodyEnd.ejs') %> +<%- include('partials/htmlFooter.ejs') %> \ No newline at end of file diff --git a/Server/views/partials/bodyEnd.ejs b/Server/views/partials/bodyEnd.ejs new file mode 100644 index 0000000..d3cfd20 --- /dev/null +++ b/Server/views/partials/bodyEnd.ejs @@ -0,0 +1,14 @@ + + + + + + + + + + \ No newline at end of file diff --git a/Server/views/partials/connectioncard.ejs b/Server/views/partials/connectioncard.ejs new file mode 100644 index 0000000..d1a0711 --- /dev/null +++ b/Server/views/partials/connectioncard.ejs @@ -0,0 +1,29 @@ +
+
+ +
+
\ No newline at end of file diff --git a/Server/views/partials/head.ejs b/Server/views/partials/head.ejs new file mode 100644 index 0000000..2aa5d1a --- /dev/null +++ b/Server/views/partials/head.ejs @@ -0,0 +1,22 @@ + + + + <% switch (page) { + case "index":%> + Node Dashboard + <% break; + case "controller":%> + Node Controller + <% break; + case "node":%> + Node Configuration + <% break; + default:%> + DRB_CnC Server + <%break; + } %> + + + + \ No newline at end of file diff --git a/Server/views/partials/htmlFooter.ejs b/Server/views/partials/htmlFooter.ejs new file mode 100644 index 0000000..62d09b8 --- /dev/null +++ b/Server/views/partials/htmlFooter.ejs @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Server/views/partials/htmlHead.ejs b/Server/views/partials/htmlHead.ejs new file mode 100644 index 0000000..89af0ef --- /dev/null +++ b/Server/views/partials/htmlHead.ejs @@ -0,0 +1,6 @@ + + + <%- include('head.ejs', {'page': page}) %> + + <%- include('navbar.ejs') %> + <%- include('sidebar.ejs') %> \ No newline at end of file diff --git a/Server/views/partials/joinModal.ejs b/Server/views/partials/joinModal.ejs new file mode 100644 index 0000000..1cc6feb --- /dev/null +++ b/Server/views/partials/joinModal.ejs @@ -0,0 +1,44 @@ + \ No newline at end of file diff --git a/Server/views/partials/modifySystemModal.ejs b/Server/views/partials/modifySystemModal.ejs new file mode 100644 index 0000000..664a755 --- /dev/null +++ b/Server/views/partials/modifySystemModal.ejs @@ -0,0 +1,61 @@ + \ No newline at end of file diff --git a/Server/views/partials/navbar.ejs b/Server/views/partials/navbar.ejs new file mode 100644 index 0000000..b9816fc --- /dev/null +++ b/Server/views/partials/navbar.ejs @@ -0,0 +1,42 @@ + \ No newline at end of file diff --git a/Server/views/partials/nodeCard.ejs b/Server/views/partials/nodeCard.ejs new file mode 100644 index 0000000..61082be --- /dev/null +++ b/Server/views/partials/nodeCard.ejs @@ -0,0 +1,48 @@ +
+
+
+ +
+
+
+ <%= node.name %> +
+ <% if(node.online){%> Online + <% } else {%> Offline + <% } %> +
+
+
+

+ + <%= node.location %> +

+

+ + + <%= node.ip %>:<%= node.port %> + +

+
+

+ + Nearby Systems: +

    + <% for(const system in node.nearbySystems){ %> +
  • <%= system %>
  • + <% } %> +
+

+
+
+
+
\ No newline at end of file diff --git a/Server/views/partials/sidebar.ejs b/Server/views/partials/sidebar.ejs new file mode 100644 index 0000000..e323e7d --- /dev/null +++ b/Server/views/partials/sidebar.ejs @@ -0,0 +1,48 @@ +
+
+ + + + +
+
+
+
+ +
+ \ No newline at end of file diff --git a/Server/views/partials/valueChip.ejs b/Server/views/partials/valueChip.ejs new file mode 100644 index 0000000..12b11ea --- /dev/null +++ b/Server/views/partials/valueChip.ejs @@ -0,0 +1,32 @@ +
+
+
+
+
+
+ <%=title%> +
+
+
+
+

+ <%=value%> +

+
+ <% if (progressPercent) {%> +
+ + <%=progressPercent%>% + +
+ <%}%> +
+ <% if (progressPercent) {%> +
+
+
+ <%}%> +
+
+
\ No newline at end of file