#37 Implement v1 Web Apps #41

Merged
logan merged 58 commits from #37-implement-webapps into master 2023-08-04 23:46:50 -04:00
18 changed files with 327 additions and 67 deletions
Showing only changes of commit ace762fc76 - Show all commits

View File

@@ -98,4 +98,4 @@ log.DEBUG(`Starting HTTP Server`);
runHTTPServer(); runHTTPServer();
log.DEBUG("Checking in with the master server") log.DEBUG("Checking in with the master server")
checkIn(); checkIn(true);

View File

@@ -1 +1 @@
{"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"}} {"Westchester Cty. Simulcast":{"frequencies":[470575000,470375000,470525000,470575000,470550000],"mode":"p25","trunkFile":"trunk.tsv"},"coppies":{"frequencies":[154690000],"mode":"nbfm","trunkFile":"none"},"asdadadadasd":{"frequencies":[123321000],"mode":"nbfm","trunkFile":"none"}}

View File

@@ -5,13 +5,14 @@ const log = new DebugBuilder("client", "clientController");
require('dotenv').config(); require('dotenv').config();
const modes = require("../config/modes"); const modes = require("../config/modes");
// Modules // Modules
const { executeAsyncConsoleCommand, nodeObject, BufferToJson } = require("../utilities/utilities"); const { executeAsyncConsoleCommand, BufferToJson } = require("../utilities/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 { updatePreset, addNewPreset, getPresets, removePreset } = require("../utilities/updatePresets");
const { onHttpError, requestOptions, sendHttpRequest } = require("../utilities/httpRequests"); 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 * Check the body for the required fields to update or add a preset
@@ -97,12 +98,11 @@ exports.checkConfig = async function checkConfig() {
* 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 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 * If the bot does not have a saved ID, it will attempt to request a new ID from the server
* *
* @param {boolean} init If set to true, the client will update the server to it's config, instead of taking the server's config * @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 (init = false) => { exports.checkIn = async (update = false) => {
let reqOptions; let reqOptions;
await this.checkConfig(); 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 // 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 { try {
if (!runningClientConfig?.id || runningClientConfig.id == 0) { if (!runningClientConfig?.id || runningClientConfig.id == 0) {
@@ -136,7 +136,7 @@ exports.checkIn = async (init = false) => {
} }
else { else {
// ID is in the config, checking in with the server // ID is in the config, checking in with the server
if (init) reqOptions = new requestOptions(`/nodes/${runningClientConfig.id}`, "PUT"); if (update) reqOptions = new requestOptions(`/nodes/${runningClientConfig.id}`, "PUT");
else reqOptions = new requestOptions(`/nodes/nodeCheckIn/${runningClientConfig.id}`, "POST"); else reqOptions = new requestOptions(`/nodes/nodeCheckIn/${runningClientConfig.id}`, "POST");
sendHttpRequest(reqOptions, JSON.stringify(runningClientConfig), (responseObject) => { sendHttpRequest(reqOptions, JSON.stringify(runningClientConfig), (responseObject) => {
log.DEBUG("Check In Respose: ", responseObject); log.DEBUG("Check In Respose: ", responseObject);
@@ -156,6 +156,8 @@ exports.checkIn = async (init = false) => {
} }
if (responseObject.statusCode === 200) { if (responseObject.statusCode === 200) {
// Server accepted the response but there were no keys to be updated // Server accepted the response but there were no keys to be updated
const tempUpdatedConfig = updateClientConfig(responseObject.body);
if (!tempUpdatedConfig.length > 0) return;
} }
if (responseObject.statusCode >= 300) { if (responseObject.statusCode >= 300) {
// Server threw an error // Server threw an error
@@ -192,6 +194,7 @@ exports.updatePreset = async (req, res) => {
checkBodyForPresetFields(req, res, () => { checkBodyForPresetFields(req, res, () => {
updatePreset(req.body.systemName, () => { updatePreset(req.body.systemName, () => {
runningClientConfig.nearbySystems = getPresets(); runningClientConfig.nearbySystems = getPresets();
this.checkIn(true);
return res.sendStatus(200); 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});
}) })
@@ -204,6 +207,7 @@ exports.addNewPreset = async (req, res) => {
checkBodyForPresetFields(req, res, () => { checkBodyForPresetFields(req, res, () => {
addNewPreset(req.body.systemName, req.body.frequencies, req.body.mode, () => { addNewPreset(req.body.systemName, req.body.frequencies, req.body.mode, () => {
runningClientConfig.nearbySystems = getPresets(); runningClientConfig.nearbySystems = getPresets();
this.checkIn(true);
return res.sendStatus(200); return res.sendStatus(200);
}, req.body.trunkFile); }, req.body.trunkFile);
}); });
@@ -217,6 +221,7 @@ exports.removePreset = async (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, () => { removePreset(req.body.systemName, () => {
runningClientConfig.nearbySystems = getPresets(); runningClientConfig.nearbySystems = getPresets();
this.checkIn(true);
return res.sendStatus(200); return res.sendStatus(200);
}, req.body.trunkFile); }, req.body.trunkFile);
}); });

View File

@@ -11,6 +11,11 @@ const { requestCheckIn, getPresets, updatePreset, addNewPreset, removePreset, up
*/ */
router.get('/requestCheckIn', requestCheckIn); router.get('/requestCheckIn', requestCheckIn);
/** GET Object of all known presets
* Query the client to get all the known presets
*/
router.put('/', );
/** GET Object of all known presets /** GET Object of all known presets
* Query the client to get all the known presets * Query the client to get all the known presets
*/ */

View File

@@ -2,6 +2,8 @@
const { DebugBuilder } = require("../utilities/debugBuilder.js"); const { DebugBuilder } = require("../utilities/debugBuilder.js");
const log = new DebugBuilder("client", "configController"); const log = new DebugBuilder("client", "configController");
// Modules // Modules
const { nodeObject } = require("./utilities.js");
const { getPresets } = require("../utilities/updatePresets");
const { readFileSync } = require('fs'); const { readFileSync } = require('fs');
const path = require("path"); const path = require("path");
require('dotenv').config(); require('dotenv').config();
@@ -33,4 +35,8 @@ function getDeviceName(){
log.DEBUG("Device Name: ", DeviceName); log.DEBUG("Device Name: ", DeviceName);
return DeviceName; return DeviceName;
} }
exports.getDeviceName = getDeviceID; 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()});
}

View File

@@ -9,7 +9,7 @@ const { isJsonString } = require("./utilities.js");
exports.requestOptions = class requestOptions { exports.requestOptions = class requestOptions {
constructor(path, method, hostname = undefined, headers = undefined, port = undefined) { 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); log.VERBOSE("Hostname Vars: ", hostname, process.env.SERVER_HOSTNAME, process.env.SERVER_IP);
if (hostname) this.hostname = hostname; if (hostname) this.hostname = hostname;
if (process.env.SERVER_HOSTNAME) this.hostname = process.env.SERVER_HOSTNAME; if (process.env.SERVER_HOSTNAME) this.hostname = process.env.SERVER_HOSTNAME;

View File

@@ -3,6 +3,7 @@ const { DebugBuilder } = require("../utilities/debugBuilder.js");
const log = new DebugBuilder("client", "updateConfig"); const log = new DebugBuilder("client", "updateConfig");
// Modules // Modules
const replace = require('replace-in-file'); const replace = require('replace-in-file');
const { getFullConfig } = require("./configHandler.js");
class Options { class Options {
constructor(key, updatedValue) { constructor(key, updatedValue) {
@@ -23,6 +24,56 @@ exports.updateId = (updatedId) => {
this.updateConfig('CLIENT_ID', updatedId); this.updateConfig('CLIENT_ID', updatedId);
} }
/**
* Wrapper to update any or all keys in the client config
*
* @param {*} configObject Object with what keys you wish to update (node object format, will be converted)
* @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});
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});
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});
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});
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});
log.DEBUG("Updated location to: ", configObject.location);
}
}
return updatedKeys;
}
/** /**
* *
* @param {string} key The config file key to update with the value * @param {string} key The config file key to update with the value
@@ -37,7 +88,6 @@ exports.updateConfig = function updateConfig(key, value) {
}) })
} }
/** /**
* Wrapper to write changes to the file * Wrapper to write changes to the file
* @param options An instance of the Objects class specified to the key being updated * @param options An instance of the Objects class specified to the key being updated

View File

@@ -25,17 +25,15 @@ exports.nodeObject = class nodeObject {
* @param {*} param0._ip The IP that the master can contact the node at * @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._port The port that the client is listening on
* @param {*} param0._location The physical location of the node * @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 * @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.id = _id;
this.name = _name; this.name = _name;
this.ip = _ip; this.ip = _ip;
this.port = _port; this.port = _port;
this.location = _location; this.location = _location;
this.nearbySystems = _nearbySystems; this.nearbySystems = _nearbySystems;
this.online = _online;
} }
} }

View File

@@ -86,6 +86,8 @@ exports.leaveServer = async (req, res) => {
const currentConnection = await getConnectionByNodeId(nodeId); const currentConnection = await getConnectionByNodeId(nodeId);
log.DEBUG("Current Connection for node: ", currentConnection); log.DEBUG("Current Connection for node: ", currentConnection);
if (!currentConnection) return res.status(400).json("Node is not connected")
await leaveServerWrapper(currentConnection.clientObject) await leaveServerWrapper(currentConnection.clientObject)
return res.status(200).json(currentConnection.clientObject.name); return res.status(200).json(currentConnection.clientObject.name);

View File

@@ -20,7 +20,7 @@ async function checkInWithNode(node) {
sendHttpRequest(reqOptions, "", (responseObj) => { sendHttpRequest(reqOptions, "", (responseObj) => {
if (responseObj) { if (responseObj) {
log.DEBUG("Response from: ", node.name, responseObj); log.DEBUG("Response from: ", node.name, responseObj);
const onlineNode = new nodeObject({ _online: false, _id: node.id }); const onlineNode = new nodeObject({ _online: true, _id: node.id });
log.DEBUG("Node update object: ", onlineNode); log.DEBUG("Node update object: ", onlineNode);
updateNodeInfo(onlineNode, (sqlResponse) => { updateNodeInfo(onlineNode, (sqlResponse) => {
if (!sqlResponse) this.log.ERROR("No response from SQL object"); if (!sqlResponse) this.log.ERROR("No response from SQL object");
@@ -193,6 +193,39 @@ exports.updateNodeSystem = async (req, res) => {
}) })
} }
/** 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 /** Updates the information received from the client based on ID
* *
* @param {*} req Default express req from router * @param {*} req Default express req from router
@@ -247,14 +280,14 @@ exports.updateExistingNode = async = (req, res) => {
if (!nodeInfo) { if (!nodeInfo) {
log.WARN("No existing node found with this ID, adding node: ", checkInObject); log.WARN("No existing node found with this ID, adding node: ", checkInObject);
addNewNode(checkInObject, (newNode) => { addNewNode(checkInObject, async (newNode) => {
this.requestNodeCheckIn({"params": {"nodeId": checkInObject.id}}); await checkInWithNode(newNode);
return res.status(201).json({ "updatedKeys": newNode }); return res.status(201).json({ "updatedKeys": newNode });
}); });
} }
else { else {
updateNodeInfo(checkInObject, () => { updateNodeInfo(checkInObject, async () => {
this.requestNodeCheckIn({"params": {"nodeId": checkInObject.id}}); await checkInWithNode(nodeInfo);
return res.status(202).json({ "updatedKeys": checkInObject }); return res.status(202).json({ "updatedKeys": checkInObject });
}); });
} }
@@ -291,7 +324,7 @@ exports.requestNodeCheckIn = async (req, res) => {
const node = await getNodeInfoFromId(req.params.nodeId); const node = await getNodeInfoFromId(req.params.nodeId);
if (!node) return res.status(400).json("No Node with the ID given"); if (!node) return res.status(400).json("No Node with the ID given");
await checkInWithNode(node); await checkInWithNode(node);
res.sendStatus(200); if (res) res.sendStatus(200);
} }
/** /**

View File

@@ -1,11 +1,119 @@
$(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);
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 * Remove a frequency input from the DOM
* *
* @param {string} system The system name to add the frequency to * @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 * @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){ function addFrequencyInput(system, inputId = null) {
if (!inputId) inputId = $(`[id^="${system}_systemFreqRow_"]`).length; if (!inputId) inputId = $(`[id^="${system}_systemFreqRow_"]`).length;
// Create new input // Create new input
var icon = document.createElement('i'); var icon = document.createElement('i');
icon.classList.add('bi'); icon.classList.add('bi');
@@ -16,7 +124,7 @@ function addFrequencyInput(system, inputId = null){
remove.classList.add('align-middle'); remove.classList.add('align-middle');
remove.classList.add('float-left'); remove.classList.add('float-left');
remove.href = '#' remove.href = '#'
remove.onclick = () => {removeFrequencyInput(`${system}_systemFreqRow_${inputId}`)} remove.onclick = () => { removeFrequencyInput(`${system}_systemFreqRow_${inputId}`) }
remove.appendChild(icon); remove.appendChild(icon);
var childColRemoveIcon = document.createElement('div'); var childColRemoveIcon = document.createElement('div');
@@ -31,7 +139,7 @@ function addFrequencyInput(system, inputId = null){
var childColInput = document.createElement('div'); var childColInput = document.createElement('div');
childColInput.classList.add('col-10'); childColInput.classList.add('col-10');
childColInput.appendChild(input); childColInput.appendChild(input);
var childRow = document.createElement('div'); var childRow = document.createElement('div');
childRow.classList.add("row"); childRow.classList.add("row");
childRow.classList.add("px-1"); childRow.classList.add("px-1");
@@ -47,19 +155,30 @@ function addFrequencyInput(system, inputId = null){
document.getElementById(`frequencyRow_${system.replaceAll(" ", "_")}`).appendChild(colParent); document.getElementById(`frequencyRow_${system.replaceAll(" ", "_")}`).appendChild(colParent);
} }
function createToast(notificationMessage){ /**
* 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'); const toastTitle = document.createElement('strong');
toastTitle.classList.add('me-auto'); toastTitle.classList.add('me-auto');
toastTitle.appendChild(document.createTextNode("Server Notification")); toastTitle.appendChild(document.createTextNode("Server Notification"));
const toastTime = document.createElement('small'); const toastTime = document.createElement('small');
toastTime.appendChild(document.createTextNode(new Date(Date.now()).toLocaleString())); toastTime.appendChild(document.createTextNode(time.toLocaleString()));
const toastClose = document.createElement('button'); const toastClose = document.createElement('button');
toastClose.type = 'button'; toastClose.type = 'button';
toastClose.classList.add('btn-close'); toastClose.classList.add('btn-close');
toastClose.ariaLabel = 'Close'; toastClose.ariaLabel = 'Close';
toastClose.setAttribute('data-bs-dismiss', 'toast'); toastClose.setAttribute('data-bs-dismiss', 'toast');
toastClose.onclick = () => { removeToastFromStorage(time.toISOString(), notificationMessage); };
const toastHeader = document.createElement('div'); const toastHeader = document.createElement('div');
toastHeader.classList.add('toast-header'); toastHeader.classList.add('toast-header');
@@ -67,7 +186,6 @@ function createToast(notificationMessage){
toastHeader.appendChild(toastTime); toastHeader.appendChild(toastTime);
toastHeader.appendChild(toastClose); toastHeader.appendChild(toastClose);
const toastMessage = document.createElement('p'); const toastMessage = document.createElement('p');
toastMessage.classList.add("px-2"); toastMessage.classList.add("px-2");
toastMessage.appendChild(document.createTextNode(notificationMessage)); toastMessage.appendChild(document.createTextNode(notificationMessage));
@@ -78,31 +196,39 @@ function createToast(notificationMessage){
const wrapperDiv = document.createElement('div'); const wrapperDiv = document.createElement('div');
wrapperDiv.classList.add('toast'); wrapperDiv.classList.add('toast');
wrapperDiv.role ='alert'; //wrapperDiv.classList.add('position-fixed');
wrapperDiv.id = `${time.toISOString()}-toast`;
wrapperDiv.role = 'alert';
wrapperDiv.ariaLive = 'assertive'; wrapperDiv.ariaLive = 'assertive';
wrapperDiv.ariaAtomic = true; wrapperDiv.ariaAtomic = true;
wrapperDiv.setAttribute('data-bs-delay', "7500");
wrapperDiv.setAttribute('data-bs-animation', true);
wrapperDiv.appendChild(toastHeader); wrapperDiv.appendChild(toastHeader);
wrapperDiv.appendChild(toastMessage); wrapperDiv.appendChild(toastMessage);
document.getElementById("toastZone").appendChild(wrapperDiv); document.getElementById("toastZone").appendChild(wrapperDiv);
addToastToStorage(time.toISOString(), notificationMessage);
$('.toast').toast('show'); if (showNow) {
return $('.toast'); const toastElement = bootstrap.Toast.getOrCreateInstance(document.getElementById(`${time.toISOString()}-toast`));
toastElement.show();
}
return;
} }
function sendNodeHeartbeat(nodeId){ function sendNodeHeartbeat(nodeId) {
const Http = new XMLHttpRequest(); const Http = new XMLHttpRequest();
const url='/nodes/nodeCheckIn/'+nodeId; const url = '/nodes/nodeCheckIn/' + nodeId;
Http.open("GET", url); Http.open("GET", url);
Http.send(); Http.send();
Http.onloadend = (e) => { Http.onloadend = (e) => {
console.log(Http.responseText) console.log(Http.responseText)
createToast(Http.responseText); createToast(Http.responseText, { showNow: true });
} }
} }
function joinServer(){ function joinServer() {
const preset = document.getElementById("selectRadioPreset").value; const preset = document.getElementById("selectRadioPreset").value;
const nodeId = document.getElementById("nodeId").value; const nodeId = document.getElementById("nodeId").value;
const clientId = document.getElementById("inputDiscordClientId").value; const clientId = document.getElementById("inputDiscordClientId").value;
@@ -118,7 +244,7 @@ function joinServer(){
console.log(reqBody); console.log(reqBody);
const Http = new XMLHttpRequest(); const Http = new XMLHttpRequest();
const url='/admin/join'; const url = '/admin/join';
Http.open("POST", url); Http.open("POST", url);
Http.setRequestHeader("Content-Type", "application/json"); Http.setRequestHeader("Content-Type", "application/json");
Http.send(JSON.stringify(reqBody)); Http.send(JSON.stringify(reqBody));
@@ -127,19 +253,19 @@ function joinServer(){
const responseObject = JSON.parse(Http.responseText) const responseObject = JSON.parse(Http.responseText)
console.log(Http.status); console.log(Http.status);
console.log(responseObject); console.log(responseObject);
createToast(`${responseObject.name} will join shortly`); createToast(`${responseObject.name} will join shortly`, { showNow: true });
$("#joinModal").modal('toggle'); $("#joinModal").modal('toggle');
} }
} }
function leaveServer(){ function leaveServer() {
const nodeId = document.getElementById("nodeId").value; const nodeId = document.getElementById("nodeId").value;
const reqBody = { const reqBody = {
'nodeId': nodeId 'nodeId': nodeId
}; };
const Http = new XMLHttpRequest(); const Http = new XMLHttpRequest();
const url='/admin/leave'; const url = '/admin/leave';
Http.open("POST", url); Http.open("POST", url);
Http.setRequestHeader("Content-Type", "application/json"); Http.setRequestHeader("Content-Type", "application/json");
Http.send(JSON.stringify(reqBody)); Http.send(JSON.stringify(reqBody));
@@ -148,8 +274,7 @@ function leaveServer(){
const responseObject = JSON.parse(Http.responseText) const responseObject = JSON.parse(Http.responseText)
console.log(Http.status); console.log(Http.status);
console.log(responseObject); console.log(responseObject);
createToast(`${responseObject} is leaving`); createToast(`${responseObject} is leaving`, { showNow: true });
setTimeout(() => {}, 45000);
} }
} }
@@ -171,7 +296,7 @@ function saveNodeDetails() {
console.log("Request Body: ", reqBody); console.log("Request Body: ", reqBody);
const Http = new XMLHttpRequest(); const Http = new XMLHttpRequest();
const url='/nodes/'+nodeId; const url = '/nodes/' + nodeId;
Http.open("PUT", url); Http.open("PUT", url);
Http.setRequestHeader("Content-Type", "application/json"); Http.setRequestHeader("Content-Type", "application/json");
Http.send(JSON.stringify(reqBody)); Http.send(JSON.stringify(reqBody));
@@ -181,15 +306,17 @@ function saveNodeDetails() {
console.log(Http.status); console.log(Http.status);
console.log(responseObject); console.log(responseObject);
createToast(`Node Updated!`); createToast(`Node Updated!`);
location.reload();
} }
} }
function addNewSystem(systemName) { function addNewSystem() {
const nodeId = document.getElementById("nodeId").value; const nodeId = document.getElementById("nodeId").value;
const systemMode = document.getElementById(`${systemName}_systemMode`).value; const systemName = document.getElementById(`New System_systemName`).value;
const inputSystemFreqs = $(`[id^="${systemName}_systemFreq_"]`); const systemMode = document.getElementById(`New System_systemMode`).value;
const inputSystemFreqs = $(`[id^="New System_systemFreq_"]`);
let systemFreqs = []; let systemFreqs = [];
for (const inputFreq of inputSystemFreqs){ for (const inputFreq of inputSystemFreqs) {
systemFreqs.push(inputFreq.value); systemFreqs.push(inputFreq.value);
} }
@@ -201,13 +328,13 @@ function addNewSystem(systemName) {
console.log("Request Body: ", reqBody); console.log("Request Body: ", reqBody);
const Http = new XMLHttpRequest(); const Http = new XMLHttpRequest();
const url='/nodes/'+nodeId+"/systems"; const url = '/nodes/' + nodeId + "/systems";
Http.open("POST", url); Http.open("POST", url);
Http.setRequestHeader("Content-Type", "application/json"); Http.setRequestHeader("Content-Type", "application/json");
Http.send(JSON.stringify(reqBody)); Http.send(JSON.stringify(reqBody));
Http.onloadend = (e) => { Http.onloadend = (e) => {
const responseObject = JSON.parse(Http.responseText) const responseObject = Http.responseText
console.log(Http.status); console.log(Http.status);
console.log(responseObject); console.log(responseObject);
createToast(`${systemName} Added!`); createToast(`${systemName} Added!`);
@@ -218,9 +345,9 @@ function addNewSystem(systemName) {
function updateSystem(systemName) { function updateSystem(systemName) {
const nodeId = document.getElementById("nodeId").value; const nodeId = document.getElementById("nodeId").value;
const systemMode = document.getElementById(`${systemName}_systemMode`).value; const systemMode = document.getElementById(`${systemName}_systemMode`).value;
const inputSystemFreqs = $(`[id^="${systemName}_systemFreq_"]`); const inputSystemFreqs = $(`[id^="${systemName}_systemFreq_"]`);
let systemFreqs = []; let systemFreqs = [];
for (const inputFreq of inputSystemFreqs){ for (const inputFreq of inputSystemFreqs) {
systemFreqs.push(inputFreq.value); systemFreqs.push(inputFreq.value);
} }
@@ -232,13 +359,13 @@ function updateSystem(systemName) {
console.log("Request Body: ", reqBody); console.log("Request Body: ", reqBody);
const Http = new XMLHttpRequest(); const Http = new XMLHttpRequest();
const url='/nodes/'+nodeId+"/systems"; const url = '/nodes/' + nodeId + "/systems";
Http.open("PUT", url); Http.open("PUT", url);
Http.setRequestHeader("Content-Type", "application/json"); Http.setRequestHeader("Content-Type", "application/json");
Http.send(JSON.stringify(reqBody)); Http.send(JSON.stringify(reqBody));
Http.onloadend = (e) => { Http.onloadend = (e) => {
const responseObject = JSON.parse(Http.responseText) const responseObject = Http.responseText;
console.log(Http.status); console.log(Http.status);
console.log(responseObject); console.log(responseObject);
createToast(`${systemName} Updated!`); createToast(`${systemName} Updated!`);
@@ -246,6 +373,28 @@ function updateSystem(systemName) {
} }
} }
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 requestNodeUpdate() {
} }

View File

@@ -18,6 +18,9 @@ router.post('/:nodeId/systems', nodesController.addNodeSystem);
// Update a system on an existing node // Update a system on an existing node
router.put('/:nodeId/systems', nodesController.updateNodeSystem); 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 // TODO Need to authenticate this request
/* POST a new node to the server /* POST a new node to the server
* *

View File

@@ -23,7 +23,7 @@
<!-- Checkin with client button --> <!-- Checkin with client button -->
<a type="button" class="btn btn-secondary" href="#" onclick="sendNodeHeartbeat('<%=node.id%>')">Check-in with Node</a> <a type="button" class="btn btn-secondary" href="#" onclick="sendNodeHeartbeat('<%=node.id%>')">Check-in with Node</a>
<!-- Update Client button --> <!-- Update Client button -->
<a type="button" class="btn btn-warning" href="#" onclick="requestNodeUpdate('<%=node.id%>')">Update Node</a> <a type="button" class="btn btn-warning disabled" href="#" onclick="requestNodeUpdate('<%=node.id%>')">Update Node</a>
</div> </div>
<hr> <hr>
<form> <form>
@@ -107,7 +107,7 @@
data-bs-target="#updateSystemModal_<%=system.replaceAll(" ", "_")%>"> data-bs-target="#updateSystemModal_<%=system.replaceAll(" ", "_")%>">
<i class="bi bi-pencil"></i> <i class="bi bi-pencil"></i>
</a> </a>
<a href="#" class="table-link text-danger label"> <a class="table-link text-danger label" onclick="removeSystem('<%=system%>')">
<i class="bi bi-trash"></i> <i class="bi bi-trash"></i>
</a> </a>
</td> </td>
@@ -135,5 +135,4 @@
<% // new System Modal %> <% // new System Modal %>
<%- include("partials/modifySystemModal.ejs", {'system': "New System", 'frequencies': [], 'mode': ''}) %> <%- include("partials/modifySystemModal.ejs", {'system': "New System", 'frequencies': [], 'mode': ''}) %>
<%- include('partials/bodyEnd.ejs') %> <%- include('partials/bodyEnd.ejs') %>
<script src="/res/js/node.js"></script>
<%- include('partials/htmlFooter.ejs') %> <%- include('partials/htmlFooter.ejs') %>

View File

@@ -9,4 +9,6 @@
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.7.0/jquery.min.js" <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.7.0/jquery.min.js"
integrity="sha512-3gJwYpMe3QewGELv8k/BX9vcqhryRdzRMxVfq6ngyWXwo03GFEzjsUm8Q7RZcHPHksttq7/GFoxjCVUjkjvPdw==" integrity="sha512-3gJwYpMe3QewGELv8k/BX9vcqhryRdzRMxVfq6ngyWXwo03GFEzjsUm8Q7RZcHPHksttq7/GFoxjCVUjkjvPdw=="
crossorigin="anonymous" referrerpolicy="no-referrer"></script> crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script src="/res/js/node.js"></script>

View File

@@ -1,7 +1,6 @@
<!doctype html> <!doctype html>
<html lang="en" data-bs-theme="auto"> <html lang="en" data-bs-theme="auto">
<%- include('head.ejs') %> <%- include('head.ejs') %>
<body> <body>
<div class="toast-container mx-2" id="toastZone"></div>
<%- include('navbar.ejs') %> <%- include('navbar.ejs') %>
<%- include('sidebar.ejs') %> <%- include('sidebar.ejs') %>

View File

@@ -12,7 +12,7 @@
<form> <form>
<div class="row gx-3 mb-3"> <div class="row gx-3 mb-3">
<label class="small mb-1 fs-6" for="systemName">System Name</label> <label class="small mb-1 fs-6" for="systemName">System Name</label>
<input class="form-control" id="systemName" type="text" value="<%if (!system == "New System") {%><%= system %><%} else {%>Local Radio System<%}%>"></input> <input class="form-control" id="<%=system%>_systemName" type="text" value="<%if (!system == "New System") {%><%= system %><%} else {%>Local Radio System<%}%>"></input>
</div> </div>
<div class="row gx-3 mb-3" id="frequencyRow_<%=system.replaceAll(" ", "_")%>"> <div class="row gx-3 mb-3" id="frequencyRow_<%=system.replaceAll(" ", "_")%>">
<label class="small mb-1 fs-6" for="systemFreq">Frequencies</label> <label class="small mb-1 fs-6" for="systemFreq">Frequencies</label>
@@ -54,7 +54,7 @@
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal" onclick="location.reload()">Close</button> <button type="button" class="btn btn-secondary" data-bs-dismiss="modal" onclick="location.reload()">Close</button>
<button type="button" class="btn btn-primary" data-bs-dismiss="modal" <%if(!system == "New System") {%>onclick="updateSystem('<%=system%>')"<%} else {%>onclick="addNewSystem('<%=system%>')"<%}%>>Save changes</button> <button type="button" class="btn btn-primary" <%if(!system == "New System") {%>onclick="updateSystem('<%=system%>')"<%} else {%>onclick="addNewSystem('<%=system%>')"<%}%>>Save changes</button>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -28,10 +28,10 @@
<li><a class="dropdown-item" href="#">Something else here</a></li> <li><a class="dropdown-item" href="#">Something else here</a></li>
</ul> </ul>
</li> </li>
<li class="nav-item">
<a class="nav-link disabled">Disabled</a>
</li>
*/%> */%>
<li class="nav-item">
<a class="nav-link" id="navbar-notification-bell" onclick="showStoredToasts()"><i class="bi bi-bell-fill"></i></a>
</li>
</ul> </ul>
<form class="d-flex" role="search"> <form class="d-flex" role="search">
<input class="form-control me-2" type="search" placeholder="Search" aria-label="Search"> <input class="form-control me-2" type="search" placeholder="Search" aria-label="Search">

View File

@@ -1,4 +1,12 @@
<div class="container-fluid mt-5"> <div class="container-fluid mt-5">
<div aria-live="polite" aria-atomic="true" class="position-relative">
<!-- Position it: -->
<!-- - `.toast-container` for spacing between toasts -->
<!-- - `top-0` & `end-0` to position the toasts in the upper right corner -->
<!-- - `.p-3` to prevent the toasts from sticking to the edge of the container -->
<div class="toast-container top-0 end-0 p-3 max" id="toastZone">
</div>
</div>
<div class="row flex-nowrap"> <div class="row flex-nowrap">
<div class="col-auto col-md-3 col-xl-2 px-sm-2 px-0 bg-dark sidebar-container"> <div class="col-auto col-md-3 col-xl-2 px-sm-2 px-0 bg-dark sidebar-container">
<div <div
@@ -34,6 +42,7 @@
</ul> </ul>
</div> </div>
*/%> */%>
</div> </div>
</div> </div>
<div class="col py-3"> <div class="col py-3">