7 Commits

Author SHA1 Message Date
Logan Cusano
e522326576 Additional changes for #37
- Updating side bar
- Updating nav bar
- Adding node details page
- Adding controller page
- Updating routes
2023-07-15 23:30:41 -04:00
Logan Cusano
2e22fa66a6 Initial bones for #37 2023-07-15 18:16:42 -04:00
Logan Cusano
6b37a48061 Update jsDoc in utils 2023-07-15 18:15:17 -04:00
Logan Cusano
5d6d86fa47 Update jsDoc in nodeController 2023-07-15 18:14:53 -04:00
Logan Cusano
c35d3f3fa7 Update adminController to use join/leave command wrappers 2023-07-15 18:14:40 -04:00
Logan Cusano
c38bca4144 Add jsDoc to leaveServerWrapper 2023-07-15 17:58:35 -04:00
Logan Cusano
e6332dffc9 Update join command to accept a specific node ID 2023-07-15 17:58:12 -04:00
22 changed files with 914 additions and 144 deletions

View File

@@ -1,7 +1,7 @@
// Modules // Modules
const { SlashCommandBuilder } = require('discord.js'); const { SlashCommandBuilder } = require('discord.js');
const { DebugBuilder } = require("../utilities/debugBuilder"); const { DebugBuilder } = require("../utilities/debugBuilder");
const { getMembersInRole, getAllClientIds, filterAutocompleteValues } = require("../utilities/utils"); const { getMembersInRole, getAllClientIds, filterAutocompleteValues, getKeyByArrayValue } = require("../utilities/utils");
const { requestOptions, sendHttpRequest } = require("../utilities/httpRequests"); const { requestOptions, sendHttpRequest } = require("../utilities/httpRequests");
const { getOnlineNodes, updateNodeInfo, addNodeConnection, getConnectionByNodeId, getAllConnections } = require("../utilities/mysqlHandler"); const { getOnlineNodes, updateNodeInfo, addNodeConnection, getConnectionByNodeId, getAllConnections } = require("../utilities/mysqlHandler");
@@ -14,9 +14,10 @@ const log = new DebugBuilder("server", "join");
* @param {*} presetName The preset name to listen to on the client * @param {*} presetName The preset name to listen to on the client
* @param {*} channelId The channel ID to join the bot to * @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 {*} 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 * @returns
*/ */
async function joinServerWrapper(presetName, channelId, connections) { async function joinServerWrapper(presetName, channelId, connections, nodeId = 0) {
// Get nodes online // Get nodes online
var onlineNodes = await new Promise((recordResolve, recordReject) => { var onlineNodes = await new Promise((recordResolve, recordReject) => {
getOnlineNodes((nodeRows) => { getOnlineNodes((nodeRows) => {
@@ -63,7 +64,10 @@ async function joinServerWrapper(presetName, channelId, connections) {
selectedClientId = availableClientIds[0]; selectedClientId = availableClientIds[0];
} }
const selectedNode = nodesCurrentlyAvailable[0]; let selectedNode;
if (nodeId > 0) selectedNode = getKeyByArrayValue(nodesCurrentlyAvailable, nodeId);
if (!selectedNode) selectedNode = nodesCurrentlyAvailable[0];
const reqOptions = new requestOptions("/bot/join", "POST", selectedNode.ip, selectedNode.port); const reqOptions = new requestOptions("/bot/join", "POST", selectedNode.ip, selectedNode.port);
const postObject = { const postObject = {

View File

@@ -8,6 +8,11 @@ const { checkNodeConnectionByClientId, removeNodeConnectionByNodeId, getAllConne
// Global Vars // Global Vars
const log = new DebugBuilder("server", "leave"); const log = new DebugBuilder("server", "leave");
/**
*
* @param {*} clientIdObject The client ID object for the node to leave the server. Either 'clientId'||'name' can be set.
* @returns
*/
async function leaveServerWrapper(clientIdObject) { async function leaveServerWrapper(clientIdObject) {
if (!clientIdObject.clientId || !clientIdObject.name) return log.ERROR("Tried to leave server without client ID and/or Name"); if (!clientIdObject.clientId || !clientIdObject.name) return log.ERROR("Tried to leave server without client ID and/or Name");

View File

@@ -7,6 +7,9 @@ const log = new DebugBuilder("server", "adminController");
const mysqlHandler = require("../utilities/mysqlHandler"); const mysqlHandler = require("../utilities/mysqlHandler");
const utils = require("../utilities/utils"); const utils = require("../utilities/utils");
const requests = require("../utilities/httpRequests"); const requests = require("../utilities/httpRequests");
const { leaveServerWrapper } = require("../commands/leave");
const { joinServerWrapper } = require("../commands/join");
/** Get the presets of all online nodes, can be used for functions /** Get the presets of all online nodes, can be used for functions
* *
@@ -15,25 +18,8 @@ const requests = require("../utilities/httpRequests");
*/ */
async function getPresetsOfOnlineNodes(callback) { async function getPresetsOfOnlineNodes(callback) {
mysqlHandler.getOnlineNodes((onlineNodes) => { mysqlHandler.getOnlineNodes((onlineNodes) => {
let systems = {}; return callback(onlineNodes);
onlineNodes.forEach(onlineNode => {
systems[onlineNode.id] = utils.BufferToJson(onlineNode.nearbySystems);
}); });
return callback(systems);
});
}
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) { async function getNodeBotStatus(nodeId, callback) {
@@ -51,24 +37,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 /** Return to requests for the presets of all online nodes, cannot be used in functions
* *
@@ -88,42 +56,38 @@ exports.getAvailablePresets = async (req, res) => {
* @param {*} req Express request parameter * @param {*} req Express request parameter
* @var {*} req.body.preset The preset to join (REQ) * @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 * @param {*} res Express response parameter
*/ */
exports.joinPreset = async (req, res) => { exports.joinPreset = async (req, res) => {
if (!req.body.preset) return res.status(400).json("No preset specified"); if (!req.body.preset) return res.status(400).json("No preset specified");
await getPresetsOfOnlineNodes((systems) => { if (!req.body.nodeId) return res.status(400).json("No node ID specified");
const systemsWithSelectedPreset = Object.values(systems).filter(nodePresets => nodePresets.includes(req.body.preset)).length if (!req.body.clientId) return res.status(400).json("No client ID specified");
if (!systemsWithSelectedPreset) return res.status(400).json("No system online with that preset"); if (!req.body.channelId) return res.status(400).json("No channel ID specified");
if (systemsWithSelectedPreset > 1) {
if (!req.body.nodeId) return res.status(175).json("Multiple locations with the selected channel, please specify a nodeID (nodeId)") const preset = req.body.preset;
requestNodeListenToPreset(req.body.preset, req.body.nodeId, (responseObject) => { const nodeId = req.body.nodeId;
if (responseObject === false) return res.status(400).json("Timeout reached"); const clientId = req.body.clientId;
return res.sendStatus(responseObject.statusCode); const channelId = req.body.channelId;
});
} const joinedClient = await joinServerWrapper(preset, channelId, clientId, nodeId);
else { if (!joinedClient) return res.send(400).json("No joined client");
let nodeId; return res.send(200).json(joinedClient);
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);
});
}
});
} }
/** Request a node to join the server listening to a specific preset /** Request a node to join the server listening to a specific preset
* *
* @param {*} req Express request parameter * @param {*} req Express request parameter
* @param {*} res Express response parameter * @param {*} res Express response parameter
* @var {*} req.body.clientId The ID of the client to disconnect
*/ */
exports.leaveServer = async (req, res) => { exports.leaveServer = async (req, res) => {
if (!req.body.nodeId) return res.status(400).json("No nodeID specified"); if (!req.body.clientId) return res.status(400).json("No clientID specified");
requestNodeLeaveServer(req.body.nodeId, (responseObject) => { const clientId = req.body.clientId;
if (responseObject === false) return res.status(400).json("Bot not joined to server");
return res.sendStatus(responseObject.statusCode); await leaveServerWrapper(clientId)
});
return res.send(200);
} }

View File

@@ -2,18 +2,65 @@
const { DebugBuilder } = require("../utilities/debugBuilder.js"); const { DebugBuilder } = require("../utilities/debugBuilder.js");
const log = new DebugBuilder("server", "nodesController"); const log = new DebugBuilder("server", "nodesController");
// Utilities // 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 utils = require("../utilities/utils");
const { sendHttpRequest, requestOptions } = require("../utilities/httpRequests.js"); const { sendHttpRequest, requestOptions } = require("../utilities/httpRequests.js");
const { nodeObject } = require("../utilities/recordHelper.js"); const { nodeObject } = require("../utilities/recordHelper.js");
const { joinServerWrapper } = require("../commands/join");
const refreshInterval = process.env.NODE_MONITOR_REFRESH_INTERVAL ?? 1200000; const refreshInterval = process.env.NODE_MONITOR_REFRESH_INTERVAL ?? 1200000;
/** /**
* Check in with a singular node, mark it offline if it's offline and
* *
* @param {*} req * @param {*} node The node Object to check in with
* @param {*} res */
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: false, _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 Default express req from router
* @param {*} res Defualt express res from router
*/ */
exports.listAllNodes = async (req, res) => { exports.listAllNodes = async (req, res) => {
getAllNodes((allNodes) => { getAllNodes((allNodes) => {
@@ -23,7 +70,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) => { exports.newNode = async (req, res) => {
if (!req.body.name) return res.status(400).json("No name specified for new node"); if (!req.body.name) return res.status(400).json("No name specified for new node");
@@ -40,7 +91,7 @@ exports.newNode = async (req, res) => {
addNewNode(newNode, (newNodeObject) => { addNewNode(newNode, (newNodeObject) => {
// Send back a success if the user has been added and the ID for the client to keep track of // 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) { catch (err) {
@@ -53,7 +104,11 @@ 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) => { exports.getNodeInfo = async (req, res) => {
if (!req.query.id) return res.status(400).json("No id specified"); if (!req.query.id) return res.status(400).json("No id specified");
getNodeInfoFromId(req.query.id, (nodeInfo) => { getNodeInfoFromId(req.query.id, (nodeInfo) => {
@@ -61,7 +116,11 @@ exports.getNodeInfo = async (req, res) => {
}) })
} }
// 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 {*} res Defualt express res from router
*/
exports.nodeCheckIn = async (req, res) => { exports.nodeCheckIn = async (req, res) => {
if (!req.body.id) return res.status(400).json("No id specified"); if (!req.body.id) return res.status(400).json("No id specified");
getNodeInfoFromId(req.body.id, (nodeInfo) => { getNodeInfoFromId(req.body.id, (nodeInfo) => {
@@ -112,27 +171,28 @@ exports.nodeCheckIn = 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, (newNode) => {
return res.status(201).json({"updatedKeys": newNode}); return res.status(201).json({ "updatedKeys": newNode });
}); });
} }
else { else {
updateNodeInfo(checkInObject, () => { updateNodeInfo(checkInObject, () => {
return res.status(202).json({"updatedKeys": checkInObject}); return res.status(202).json({ "updatedKeys": checkInObject });
}); });
} }
}); });
} }
/** /**
* Request the node to join the specified server/channel and listen to the specified resource * Requests a specific node to check in with the server, if it's online
* *
* @param req.body.clientId The client ID to join discord with (NOT dev portal ID, right click user -> Copy ID) * @param {*} req Default express req from router
* @param req.body.channelId The Channel ID to join in Discord * @param {*} res Defualt express res from router
* @param req.body.presetName The preset name to listen to in Discord
*/ */
exports.requestNodeJoinServer = async (req, res) => { exports.requestNodeCheckIn = 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"); if (!req.params.nodeId) return res.status(400).json("No Node ID supplied in request");
await joinServerWrapper(req.body.presetName, req.body.channelId, req.body.clientId); const node = await getNodeInfoFromId(req.params.nodeId);
if (!node) return res.status(400).json("No Node with the ID given");
checkInWithNode(node);
} }
/** /**
@@ -143,46 +203,24 @@ exports.nodeMonitorService = class nodeMonitorService {
this.log = new DebugBuilder("server", "nodeMonitorService"); this.log = new DebugBuilder("server", "nodeMonitorService");
} }
async start(){ /**
* 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 // 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)); await new Promise(resolve => setTimeout(resolve, refreshInterval / 10));
log.INFO("Starting Node Monitor Service"); log.INFO("Starting Node Monitor Service");
// Check in before starting the infinite loop // Check in before starting the infinite loop
await this.checkInWithOnlineNodes(); await checkInWithOnlineNodes();
while(true){ 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 // 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 new Promise(resolve => setTimeout(resolve, refreshInterval));
await this.checkInWithOnlineNodes(); await checkInWithOnlineNodes();
await new Promise(resolve => setTimeout(resolve, refreshInterval / 4)); await new Promise(resolve => setTimeout(resolve, refreshInterval / 4));
continue; 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;
});
}
} }

View File

@@ -0,0 +1,176 @@
.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: 94.2vh;
}
/* 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;
}

View File

@@ -0,0 +1,3 @@
function sendNodeHeartbeat(nodeId) {
console.log(nodeId);
}

View File

@@ -0,0 +1,50 @@
function addFrequencyInput(system){
// 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.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 = 'nodeFreq';
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.appendChild(childRow);
document.getElementById(`frequencyRow_${system.replaceAll(" ", "_")}`).appendChild(colParent);
}
function checkInByNodeId(nodeId){
const Http = new XMLHttpRequest();
const url='/nodes/'+nodeId;
Http.open("GET", url);
Http.send();
Http.onreadystatechange = (e) => {
console.log(Http.responseText)
}
}

View File

@@ -1,11 +1,12 @@
const libCore = require("../libCore");
var express = require('express'); var express = require('express');
var router = express.Router(); var router = express.Router();
const { getAllNodes, getNodeInfoFromId } = require("../utilities/mysqlHandler");
/* GET home page. */ /* GET home page. */
router.get('/', (req, res) => { router.get('/', (req, res) => {
var sources = libCore.getSources(); //var sources = libCore.getSources();
//res.render('index', { "sources": sources }); return res.render('index');
var htmlOutput = ""; var htmlOutput = "";
@@ -28,4 +29,24 @@ router.get('/', (req, res) => {
res.send(htmlOutput); res.send(htmlOutput);
}); });
/* 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});
});
/* 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});
});
module.exports = router; module.exports = router;

View File

@@ -28,8 +28,7 @@ router.get('/nodeInfo', nodesController.getNodeInfo);
// Client checkin with the server to update information // Client checkin with the server to update information
router.post('/nodeCheckIn', nodesController.nodeCheckIn); router.post('/nodeCheckIn', nodesController.nodeCheckIn);
// TODO Need to authenticate this request // Request a node to check in with the server
// Request a particular client to join a particular channel listening to a particular preset router.get('/:nodeId', nodesController.requestNodeCheckIn);
router.post('/joinServer', nodesController.requestNodeJoinServer);
module.exports = router; module.exports = router;

View File

@@ -119,7 +119,6 @@ class nodeObject {
this.port = _port; this.port = _port;
this.location = _location; this.location = _location;
this.nearbySystems = _nearbySystems; this.nearbySystems = _nearbySystems;
if (this.nearbySystems) this.presets = Object.keys(_nearbySystems);
this.online = _online; this.online = _online;
} }
} }

View File

@@ -119,7 +119,12 @@ exports.getClientObjectByClientID = (clientId) => {
return undefined return undefined
} }
/**
* Wrapper to filter auto complete
*
* @param {*} interaction
* @param {*} options
*/
exports.filterAutocompleteValues = async (interaction, options) => { exports.filterAutocompleteValues = async (interaction, options) => {
// Get the command used // Get the command used
const command = interaction.command; const command = interaction.command;

View File

@@ -0,0 +1,11 @@
<%- include('partials/htmlHead.ejs') %>
<div class="container">
<div class="row row-cols-1 row-cols-md-2 row-cols-xl-4">
<% for(const node of nodes) {%>
<%- include('partials/nodeCard.ejs', {'node': node}) %>
<% } %>
</div>
</div>
<%- include('partials/bodyEnd.ejs') %>
<script src="/res/js/controller.js"></script>
<%- include('partials/htmlFooter.ejs') %>

View File

@@ -1,11 +1,133 @@
<!DOCTYPE html> <%- include('partials/htmlHead.ejs') %>
<html> <div class="container">
<head> <div class="row row-cols-1 row-cols-md-2 row-cols-xl-4">
<title><%= title %></title> <div class="col-xl-3 col-lg-6">
<link rel='stylesheet' href='/stylesheets/style.css' /> <div class="info-card l-bg-cherry">
</head> <div class="card-statistic p-4">
<body> <div class="card-icon card-icon-large"><i class="bi bi-cart"></i></div>
<h1><%= title %></h1> <div class="mb-4">
<p>Welcome to <%= title %></p> <h5 class="card-title mb-0">New Orders</h5>
</body> </div>
</html> <div class="row align-items-center mb-2 d-flex">
<div class="col-8">
<h2 class="d-flex align-items-center mb-0">
3,243
</h2>
</div>
<div class="col-4 text-right">
<span>12.5% <i class="fa fa-arrow-up"></i></span>
</div>
</div>
<div class="progress mt-1 " data-height="8" style="height: 8px;">
<div class="progress-bar l-bg-cyan" role="progressbar" data-width="25%" aria-valuenow="25"
aria-valuemin="0" aria-valuemax="100" style="width: 25%;"></div>
</div>
</div>
</div>
</div>
<div class="col-xl-3 col-lg-6">
<div class="info-card l-bg-blue-dark">
<div class="card-statistic p-4">
<div class="card-icon card-icon-large"><i class="bi bi-cart"></i></div>
<div class="mb-4">
<h5 class="card-title mb-0">New Orders</h5>
</div>
<div class="row align-items-center mb-2 d-flex">
<div class="col-8">
<h2 class="d-flex align-items-center mb-0">
3,243
</h2>
</div>
<div class="col-4 text-right">
<span>12.5% <i class="fa fa-arrow-up"></i></span>
</div>
</div>
<div class="progress mt-1 " data-height="8" style="height: 8px;">
<div class="progress-bar l-bg-cyan" role="progressbar" data-width="25%" aria-valuenow="25"
aria-valuemin="0" aria-valuemax="100" style="width: 25%;"></div>
</div>
</div>
</div>
</div>
<div class="col-xl-3 col-sm-6">
<div class="card node-card">
<div class="card-body">
<div class="dropdown float-end">
<a class="text-muted dropdown-toggle font-size-16" href="#" role="button"
data-bs-toggle="dropdown" aria-haspopup="true"><i
class="bx bx-dots-horizontal-rounded"></i></a>
<div class="dropdown-menu dropdown-menu-end"><a class="dropdown-item" href="#">Edit</a><a
class="dropdown-item" href="#">Action</a><a class="dropdown-item"
href="#">Remove</a></div>
</div>
<div class="d-flex align-items-center">
<div><img src="https://bootdey.com/img/Content/avatar/avatar1.png" alt=""
class="avatar-md rounded-circle img-thumbnail" /></div>
<div class="flex-1 ms-3">
<h5 class="font-size-16 mb-1"><a href="#" class="text-dark">Phyllis Gatlin</a></h5>
<span class="badge badge-soft-success mb-0">Full Stack Developer</span>
</div>
</div>
<div class="mt-3 pt-1">
<p class="text-muted mb-0"><i
class="mdi mdi-phone font-size-15 align-middle pe-2 text-primary"></i> 070 2860 5375
</p>
<p class="text-muted mb-0 mt-2"><i
class="mdi mdi-email font-size-15 align-middle pe-2 text-primary"></i>
PhyllisGatlin@spy.com</p>
<p class="text-muted mb-0 mt-2"><i
class="mdi mdi-google-maps font-size-15 align-middle pe-2 text-primary"></i> 52
Ilchester MYBSTER 9WX</p>
</div>
</div>
</div>
</div>
<div class="col-xl-3 col-sm-6">
<div class="card node-card">
<div class="card-body">
<div class="dropdown float-end">
<a class="text-muted dropdown-toggle font-size-16" href="#" role="button"
data-bs-toggle="dropdown" aria-haspopup="true"><i
class="bx bx-dots-horizontal-rounded"></i></a>
<div class="dropdown-menu dropdown-menu-end"><a class="dropdown-item" href="#">Edit</a><a
class="dropdown-item" href="#">Action</a><a class="dropdown-item"
href="#">Remove</a></div>
</div>
<div class="d-flex align-items-center">
<div><img src="https://bootdey.com/img/Content/avatar/avatar5.png" alt=""
class="avatar-md rounded-circle img-thumbnail" /></div>
<div class="flex-1 ms-3">
<h5 class="font-size-16 mb-1"><a href="#" class="text-dark">Diana Owens</a></h5>
<span class="badge badge-soft-danger mb-0">UI/UX Designer</span>
</div>
</div>
<div class="mt-3 pt-1">
<p class="text-muted mb-0"><i
class="mdi mdi-phone font-size-15 align-middle pe-2 text-primary"></i> 087 6321 3235
</p>
<p class="text-muted mb-0 mt-2"><i
class="mdi mdi-email font-size-15 align-middle pe-2 text-primary"></i>
DianaOwens@spy.com</p>
<p class="text-muted mb-0 mt-2"><i
class="mdi mdi-google-maps font-size-15 align-middle pe-2 text-primary"></i> 52
Ilchester MYBSTER 9WX</p>
</div>
<div class="d-flex gap-2 pt-4">
<button type="button" class="btn btn-soft-primary btn-sm w-50"><i
class="bx bx-user me-1"></i> Profile</button>
<button type="button" class="btn btn-primary btn-sm w-50"><i
class="bx bx-message-square-dots me-1"></i> Contact</button>
</div>
</div>
</div>
</div>
</div>
</div>
<%- include('partials/bodyEnd.ejs') %>
<%- include('partials/htmlFooter.ejs') %>

154
Server/views/node.ejs Normal file
View File

@@ -0,0 +1,154 @@
<%- include('partials/htmlHead.ejs') %>
<div class="container">
<div class="card mb-4">
<div class="card-header">
<p>
<span class="fs-2 fw-semibold">
Node Details
</span>
</p>
</div>
<div class="card-body">
<form>
<div class="row gx-3 mb-3">
<div class="">
</div>
<div class="col-md-6">
<label class="small mb-1" for="nodeId">Node ID (this is the assigned Node ID and cannot be
changed)</label>
<input class="form-control" id="nodeId" type="text" value="<%=node.id%>" disabled></input>
</div>
<div class="col-md-6">
<label class="small mb-1" for="nodeStatus">Online Status:</label>
<% if(node.online){%> <span class="badge badge-soft-success mb-0 align-middle fs-6" id="nodeStatus">Online</span>
<% } else {%> <span class="badge badge-soft-danger mb-0 align-middle fs-6">Offline</span>
<% } %>
<hr>
<!-- Join Server button-->
<a type="button" class="btn btn-info <% if(!node.online) { %>disabled<% } %>" href="/join/<%=node.id%>">Join Server</a>
<!-- Leave Server button -->
<a type="button" class="btn btn-danger <% if(!node.online) { %>disabled<% } %>" href="/leave/<%=node.id%>">Leave Server</a>
<!-- Checkin with client button -->
<a type="button" class="btn btn-secondary" href="#" onclick="checkInByNodeId('<%=node.id%>')">Check-in with Node</a>
</div>
</div>
<div class="row gx-3 mb-3">
<div class="col-md-12">
<label class="small mb-1" for="inputNodeName">Node Name:</label>
<input class="form-control" id="inputNodeName" type="text" value="<%=node.name%>"></input>
</div>
</div>
<div class="row gx-3 mb-3">
<div class="col-md-4">
<label class="small mb-1" for="inputNodeIp">Node IP Address (that the server can
contact):</label>
<input class="form-control" id="inputNodeIp" type="text" value="<%=node.ip%>"></input>
</div>
<div class="col-md-2">
<label class="small mb-1" for="inputOrgName">Node Port (with the API):</label>
<input class="form-control" id="inputOrgName" type="number" value="<%=node.port%>"></input>
</div>
</div>
<div class="mb-3">
<label class="small mb-1" for="inputNodeLocation">Node Location (physical location):</label>
<input class="form-control" id="inputNodeLocation" type="location" value="<%=node.location%>"></input>
</div>
<h4>
Nearby Systems
</h4>
<hr>
<div class="row">
<div class="col-lg-12">
<div class="main-box no-header clearfix">
<div class="main-box-body clearfix">
<div class="table-responsive">
<table class="table user-list <% if(!node.online) { %>disabled<% } %>">
<thead>
<tr>
<th><span>System Name</span></th>
<th><span>Frequencies</span></th>
<th><span>Protocol</span></th>
<th>&nbsp;</th>
</tr>
</thead>
<tbody>
<% for(const system in node.nearbySystems){ %>
<tr>
<td>
<%= system %>
</td>
<td>
<% if(node.nearbySystems[system].frequencies.length> 1) { %>
<ul>
<% for(const frequency of
node.nearbySystems[system].frequencies) { %>
<li>
<%=frequency%> MHz
</li>
<% } %>
</ul>
<% } else { const
frequency=node.nearbySystems[system].frequencies[0]
%>
<%=frequency%> MHz
<% } %>
</td>
<td>
<span class="label label-default text-uppercase">
<%= node.nearbySystems[system].mode %>
</span>
</td>
<td>
<a href="#" class="table-link text-info label"
data-bs-toggle="modal"
data-bs-target="#updateSystemModal_<%=system.replaceAll(" ", "_")%>">
<i class="bi bi-pencil"></i>
</a>
<a href="#" class="table-link text-danger label">
<i class="bi bi-trash"></i>
</a>
</td>
</tr>
<% // Update system modal %>
<%- include("partials/modifySystemModal.ejs", {'system': system, 'frequencies': node.nearbySystems[system].frequencies}) %>
<% } %>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
<!-- Save changes button-->
<button class="btn btn-primary <% if(!node.online) { %>disabled<% } %>" type="button">Save changes</button>
<!-- Button trigger modal -->
<button type="button" class="btn btn-primary float-right <% if(!node.online) { %>disabled<% } %>" data-bs-toggle="modal"
data-bs-target="#newSystemModal">Add New System</button>
</form>
</div>
</div>
</div>
<% // new System Modal %>
<div class="modal fade" id="newSystemModal" tabindex="-1" aria-labelledby="newSystemModal" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h1 class="modal-title fs-5" id="newSystemModal">Add a New System</h1>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
...
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
<button type="button" class="btn btn-primary">Save changes</button>
</div>
</div>
</div>
</div>
<%- include('partials/bodyEnd.ejs') %>
<script src="/res/js/node.js"></script>
<%- include('partials/htmlFooter.ejs') %>

View File

@@ -0,0 +1,12 @@
</div>
</div>
</div>
</body>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"
integrity="sha384-geWF76RCwLtnZ8qwWowPQNguL3RmwHVBC9FhGdlKrxdiJJigb/j/68SIy3Te4Bkz"
crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.7.0/jquery.min.js"
integrity="sha512-3gJwYpMe3QewGELv8k/BX9vcqhryRdzRMxVfq6ngyWXwo03GFEzjsUm8Q7RZcHPHksttq7/GFoxjCVUjkjvPdw=="
crossorigin="anonymous" referrerpolicy="no-referrer"></script>

View File

@@ -0,0 +1,9 @@
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Bootstrap demo</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet"
integrity="sha384-9ndCyUaIbzAi2FUVXJi0CjmCapSmO7SnpJef0486qhLnuZ2cdeRhO02iuK6FUUVM" crossorigin="anonymous">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.3.0/font/bootstrap-icons.css">
<link rel="stylesheet" href="/res/css/main.css">
</head>

View File

@@ -0,0 +1 @@
</html>

View File

@@ -0,0 +1,6 @@
<!doctype html>
<html lang="en" data-bs-theme="auto">
<%- include('head.ejs') %>
<body>
<%- include('navbar.ejs') %>
<%- include('sidebar.ejs') %>

View File

@@ -0,0 +1,54 @@
<div class="modal fade" id="updateSystemModal_<%=system.replaceAll(" ", "_")%>" tabindex="-1" aria-labelledby="updateSystemModal_<%=system.replaceAll(" ", "_")%>"
aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h1 class="modal-title fs-5" id="updateSystemModal_<%=system.replaceAll(" ", "_")%>">Update <%=system%></h1>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="card mb-4">
<div class="card-body">
<form>
<div class="row gx-3 mb-3" id="frequencyRow_<%=system.replaceAll(" ", "_")%>">
<label class="small mb-1 fs-6" for="nodeFreq">Frequencies</label>
<% for(const frequency of frequencies) { %>
<div class="col-md-6 mb-1">
<div class="row px-1">
<div class="col-10">
<input class="form-control" id="nodeFreq" type="text" value="<%= frequency %>"></input>
</div>
<div class="col-2">
<a class="align-middle float-left" href="#"><i class="bi bi-x-circle text-black"></i></a>
</div>
</div>
</div>
<% } %>
</div>
<button type="button" class="btn btn-info text-white" onclick="addFrequencyInput('<%=system%>')">Add Frequency</button>
<hr>
<div class="row gx-3 mb-3">
<div class="col-md-6">
<label class="small mb-1 fs-6" for="modeSelect">Mode</label>
<br>
<select class="custom-select" id="modeSelect">
<option value="<%= node.nearbySystems[system].mode %>" selected><span class="text-uppercase"><%= node.nearbySystems[system].mode %></span></option>
<% if(node.nearbySystems[system].mode == "p25") { %>
<option value="nbfm">NBFM</option>
<% } else if (node.nearbySystems[system].mode == "nbfm") { %>
<option value="p25">P25</option>
<% } %>
</select>
</div>
</div>
</form>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
<button type="button" class="btn btn-primary">Save changes</button>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,42 @@
<nav class="navbar navbar-expand-lg bg-body-tertiary" data-bs-theme="dark">
<div class="container-fluid">
<a class="navbar-brand" href="#">Node Master</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent"
aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarSupportedContent">
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
<% /*
<li class="nav-item">
<a class="nav-link active" aria-current="page" href="#">Home</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#">Link</a>
</li>
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown"
aria-expanded="false">
Dropdown
</a>
<ul class="dropdown-menu">
<li><a class="dropdown-item" href="#">Action</a></li>
<li><a class="dropdown-item" href="#">Another action</a></li>
<li>
<hr class="dropdown-divider">
</li>
<li><a class="dropdown-item" href="#">Something else here</a></li>
</ul>
</li>
<li class="nav-item">
<a class="nav-link disabled">Disabled</a>
</li>
*/%>
</ul>
<form class="d-flex" role="search">
<input class="form-control me-2" type="search" placeholder="Search" aria-label="Search">
<button class="btn btn-outline-success" type="submit">Search</button>
</form>
</div>
</div>
</nav>

View File

@@ -0,0 +1,57 @@
<div class="col-xl-3 col-sm-6">
<div class="card node-card">
<div class="card-body">
<div class="dropdown float-end">
<a class="text-muted dropdown-toggle font-size-16" href="#" role="button" data-bs-toggle="dropdown"
aria-haspopup="true">
<i class="bx bx-dots-horizontal-rounded"></i>
</a>
<div class="dropdown-menu dropdown-menu-end">
<a class="dropdown-item node-action" href="/node/<%=node.id%>" >Edit</a>
<a class="dropdown-item node-action" href="#" onclick="sendNodeHeartbeat('<%=node.id%>')">Send Heartbeat</a>
</div>
</div>
<div class="d-flex align-items-center">
<div>
<img src="https://bootdey.com/img/Content/avatar/avatar1.png" alt=""
class="avatar-md rounded-circle img-thumbnail" />
</div>
<div class="flex-1 ms-3">
<h5 class="font-size-16 mb-1"><a href="/node/<%=node.id%>" class="text-dark">
<%= node.name %>
</a></h5>
<% if(node.online){%> <span class="badge badge-soft-success mb-0">Online</span>
<% } else {%> <span class="badge badge-soft-danger mb-0">Offline</span>
<% } %>
</div>
</div>
<div class="mt-3 pt-1">
<p class="text-muted mb-0">
<i class="bi bi-geo font-size-15 align-middle pe-2 text-primary"></i>
<%= node.location %>
</p>
<p class="text-muted mb-0">
<i class="bi bi-phone font-size-15 align-middle pe-2 text-primary"></i>
<a href="#" target="_blank">
<%= node.ip %>:<%= node.port %>
</a>
</p>
<p class="text-muted mb-0">
<div class="dropdown">
<i class="bi bi-broadcast-pin font-size-15 align-middle pe-2 text-primary"></i>
<a class="dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">
Nearby Systems
</a>
<ul class="dropdown-menu">
<% for(const system in node.nearbySystems){ %>
<li>
<%= system %>
</li>
<% } %>
</ul>
</div>
</p>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,38 @@
<div class="container-fluid">
<div class="row flex-nowrap">
<div class="col-auto col-md-3 col-xl-2 px-sm-2 px-0 bg-dark">
<div class="d-flex flex-column align-items-center align-items-sm-start px-3 pt-2 text-white sidebar-container">
<ul class="nav nav-pills flex-column mb-sm-auto mb-0 align-items-center align-items-sm-start" id="menu">
<li>
<a href="/" class="nav-link px-0 align-middle">
<i class="fs-4 bi-speedometer2"></i> <span class="ms-1 d-none d-sm-inline">Dashboard</span>
</a>
</li>
<li>
<a href="/controller" class="nav-link px-0 align-middle">
<i class="fs-4 bi-grid"></i> <span class="ms-1 d-none d-sm-inline">Controller</span> </a>
</li>
</ul>
<hr>
<% /*
<div class="dropdown pb-4 fixed-bottom px-3">
<a href="#" class="d-flex align-items-center text-white text-decoration-none dropdown-toggle"
id="dropdownUser1" data-bs-toggle="dropdown" aria-expanded="false">
<img src="https://github.com/mdo.png" alt="hugenerd" width="30" height="30"
class="rounded-circle">
<span class="d-none d-sm-inline mx-1">loser</span>
</a>
<ul class="dropdown-menu dropdown-menu-dark text-small shadow">
<li><a class="dropdown-item" href="#">New project...</a></li>
<li><a class="dropdown-item" href="#">Settings</a></li>
<li><a class="dropdown-item" href="#">Profile</a></li>
<li>
<hr class="dropdown-divider">
</li>
<li><a class="dropdown-item" href="#">Sign out</a></li>
</ul>
</div>
*/%>
</div>
</div>
<div class="col py-3">