Compare commits
57 Commits
c0927601b9
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ca2815ab8f | ||
|
|
556697725a | ||
|
|
b448f04aec | ||
|
|
fae8417b2f | ||
|
|
e06cc4762d | ||
|
|
6deba2bad2 | ||
|
|
e0d1a4a2fe | ||
| 1078faa766 | |||
|
|
75580c0547 | ||
|
|
880f1ccb01 | ||
|
|
76c4d002a0 | ||
|
|
2260deee01 | ||
|
|
8a0baa5bc9 | ||
|
|
ec091c0017 | ||
|
|
a5996ccfc0 | ||
|
|
3b248e36ec | ||
|
|
abdb725964 | ||
|
|
167f87128e | ||
|
|
bc09840dda | ||
|
|
c680c8fb2c | ||
|
|
4ceb71bd84 | ||
|
|
4b86621626 | ||
|
|
d847aa4fc7 | ||
|
|
9ff87403b2 | ||
|
|
cf9deb4841 | ||
|
|
58b4b7ff40 | ||
|
|
6b4ffc88b3 | ||
|
|
0f114066a6 | ||
|
|
648782658c | ||
|
|
d7ea6bbbd4 | ||
|
|
6ffa12911a | ||
|
|
61d7b69c10 | ||
|
|
c14316933b | ||
|
|
f55361575e | ||
|
|
c5f7cc1da6 | ||
|
|
02854fb783 | ||
|
|
4a54be7e51 | ||
|
|
cfeea57744 | ||
|
|
0a8dc75a93 | ||
|
|
0426f5eb27 | ||
|
|
d4b974f81b | ||
|
|
d05c266f75 | ||
|
|
57fa6be110 | ||
|
|
f5d58d45da | ||
|
|
62c0504028 | ||
|
|
5dd27f0bed | ||
|
|
e0bae665ed | ||
|
|
598c802b28 | ||
|
|
ace762fc76 | ||
|
|
75156d059e | ||
|
|
abb833840a | ||
|
|
11c8a149bb | ||
|
|
9c111eda1a | ||
|
|
31de3a040d | ||
|
|
318ee7bf91 | ||
|
|
5428ac6144 | ||
|
|
e27dd9d9cb |
@@ -17,4 +17,7 @@ SERVER_HOSTNAME=""
|
|||||||
SERVER_PORT=3000
|
SERVER_PORT=3000
|
||||||
|
|
||||||
# Configuration of the local OP25 application
|
# Configuration of the local OP25 application
|
||||||
OP25_BIN_PATH=""
|
#OP25_BIN_PATH=""
|
||||||
|
|
||||||
|
# Logfile location config
|
||||||
|
#LOG_LOCATION=""
|
||||||
1
Client/.gitignore
vendored
Normal file
1
Client/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
*radioPresets.json
|
||||||
@@ -17,7 +17,7 @@ var { attachRadioSessionToRequest } = require('./controllers/radioController');
|
|||||||
const log = new DebugBuilder("client", "app");
|
const log = new DebugBuilder("client", "app");
|
||||||
|
|
||||||
var app = express();
|
var app = express();
|
||||||
var port = process.env.HTTP_PORT || '3010';
|
var port = process.env.CLIENT_PORT || '3010';
|
||||||
|
|
||||||
// view engine setup
|
// view engine setup
|
||||||
app.set('views', path.join(__dirname, 'views'));
|
app.set('views', path.join(__dirname, 'views'));
|
||||||
@@ -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);
|
||||||
|
|||||||
@@ -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"}}
|
|
||||||
18
Client/config/radioPresets.json.EXAMPLE
Normal file
18
Client/config/radioPresets.json.EXAMPLE
Normal file
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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, nodeObject } = 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
|
||||||
@@ -94,13 +95,14 @@ exports.checkConfig = async function checkConfig() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Check in with the server
|
/** 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
|
* 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;
|
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) {
|
||||||
@@ -134,7 +136,8 @@ exports.checkIn = async () => {
|
|||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
// ID is in the config, checking in with the server
|
// 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) => {
|
sendHttpRequest(reqOptions, JSON.stringify(runningClientConfig), (responseObject) => {
|
||||||
log.DEBUG("Check In Respose: ", responseObject);
|
log.DEBUG("Check In Respose: ", responseObject);
|
||||||
// Check if the server responded
|
// Check if the server responded
|
||||||
@@ -153,6 +156,10 @@ exports.checkIn = async () => {
|
|||||||
}
|
}
|
||||||
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
|
||||||
|
if (!update){
|
||||||
|
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
|
||||||
@@ -174,11 +181,30 @@ exports.requestCheckIn = async (req, res) => {
|
|||||||
return res.sendStatus(200);
|
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
|
/** Controller for the /client/presets endpoint
|
||||||
* This is the endpoint wrapper to get the presets object
|
* This is the endpoint wrapper to get the presets object
|
||||||
*/
|
*/
|
||||||
exports.getPresets = async (req, res) => {
|
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
|
/** Controller for the /client/updatePreset endpoint
|
||||||
@@ -187,6 +213,8 @@ exports.getPresets = async (req, res) => {
|
|||||||
exports.updatePreset = async (req, res) => {
|
exports.updatePreset = async (req, res) => {
|
||||||
checkBodyForPresetFields(req, res, () => {
|
checkBodyForPresetFields(req, res, () => {
|
||||||
updatePreset(req.body.systemName, () => {
|
updatePreset(req.body.systemName, () => {
|
||||||
|
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 });
|
||||||
})
|
})
|
||||||
@@ -198,6 +226,8 @@ exports.updatePreset = async (req, res) => {
|
|||||||
exports.addNewPreset = async (req, res) => {
|
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();
|
||||||
|
this.checkIn(true);
|
||||||
return res.sendStatus(200);
|
return res.sendStatus(200);
|
||||||
}, req.body.trunkFile);
|
}, req.body.trunkFile);
|
||||||
});
|
});
|
||||||
@@ -210,6 +240,8 @@ exports.removePreset = async (req, res) => {
|
|||||||
checkBodyForPresetFields(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, () => {
|
removePreset(req.body.systemName, () => {
|
||||||
|
runningClientConfig.nearbySystems = getPresets();
|
||||||
|
this.checkIn(true);
|
||||||
return res.sendStatus(200);
|
return res.sendStatus(200);
|
||||||
}, req.body.trunkFile);
|
}, req.body.trunkFile);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -8,20 +8,21 @@ async def load_opus():
|
|||||||
# Check the system type and load the correct library
|
# Check the system type and load the correct library
|
||||||
# Linux ARM AARCH64 running 32bit OS
|
# Linux ARM AARCH64 running 32bit OS
|
||||||
processor = platform.machine()
|
processor = platform.machine()
|
||||||
|
script_dir = os.path.dirname(os.path.abspath(__file__))
|
||||||
print("Processor: ", processor)
|
print("Processor: ", processor)
|
||||||
if os.name == 'nt':
|
if os.name == 'nt':
|
||||||
if processor == "AMD64":
|
if processor == "AMD64":
|
||||||
|
opus.load_opus(os.path.join(script_dir, './opus/libopus_amd64.dll'))
|
||||||
print(f"Loaded OPUS library for AMD64")
|
print(f"Loaded OPUS library for AMD64")
|
||||||
opus.load_opus('./opus/libopus_amd64.dll')
|
|
||||||
return "AMD64"
|
return "AMD64"
|
||||||
else:
|
else:
|
||||||
if processor == "aarch64":
|
if processor == "aarch64":
|
||||||
|
opus.load_opus(os.path.join(script_dir, './opus/libopus_aarcch64.so'))
|
||||||
print(f"Loaded OPUS library for aarch64")
|
print(f"Loaded OPUS library for aarch64")
|
||||||
opus.load_opus('./opus/libopus_aarcch64.so')
|
|
||||||
return "aarch64"
|
return "aarch64"
|
||||||
elif processor == "armv7l":
|
elif processor == "armv7l":
|
||||||
|
opus.load_opus(os.path.join(script_dir, './opus/libopus_armv7l.so'))
|
||||||
print(f"Loaded OPUS library for armv7l")
|
print(f"Loaded OPUS library for armv7l")
|
||||||
opus.load_opus('./opus/libopus_armv7l.so')
|
|
||||||
return "armv7l"
|
return "armv7l"
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
183
Client/public/res/css/main.css
Normal file
183
Client/public/res/css/main.css
Normal file
@@ -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;
|
||||||
|
}
|
||||||
402
Client/public/res/js/node.js
Normal file
402
Client/public/res/js/node.js
Normal file
@@ -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();
|
||||||
|
}
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
body {
|
|
||||||
padding: 50px;
|
|
||||||
font: 14px "Lucida Grande", Helvetica, Arial, sans-serif;
|
|
||||||
}
|
|
||||||
|
|
||||||
a {
|
|
||||||
color: #00B7FF;
|
|
||||||
}
|
|
||||||
@@ -2,13 +2,13 @@
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
Explanation here
|
The client application communicates with the server through the provided API. Each client instance waits for join requests sent by users through Discord. Once a join request is received, the client uses the SDR application to tune into the specified radio preset. It then establishes a connection to Discord, allowing users to listen to the selected radio preset in real-time.
|
||||||
|
|
||||||
|
In addition to its interaction with the server, the client also has its own API and web application. This enables users to directly interface with the client, perform actions specific to the client application, and access relevant information about the connected SDR and radio presets.
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
Requirements here (not modules, that will be installed with npm)
|
|
||||||
### Hardware
|
### Hardware
|
||||||
|
|
||||||
- SBC
|
- SBC
|
||||||
|
|||||||
@@ -15,7 +15,9 @@ router.get('/status', botController.getStatus);
|
|||||||
*
|
*
|
||||||
* @param req The request sent from the master
|
* @param req The request sent from the master
|
||||||
* @param req.body.channelId The channel ID to join
|
* @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.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);
|
router.post('/join', botController.joinServer);
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
const express = require('express');
|
const express = require('express');
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
// Controllers
|
// 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
|
/** GET Request a check in from the client
|
||||||
* Queue the client to check in with the server
|
* Queue the client to check in with the server
|
||||||
@@ -16,6 +16,16 @@ router.get('/requestCheckIn', requestCheckIn);
|
|||||||
*/
|
*/
|
||||||
router.get('/presets', getPresets);
|
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
|
/** POST Update to preset
|
||||||
*
|
*
|
||||||
* @param req The request sent from the master
|
* @param req The request sent from the master
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
var express = require('express');
|
var express = require('express');
|
||||||
var router = express.Router();
|
var router = express.Router();
|
||||||
|
const { getFullConfig } = require('../utilities/configHandler');
|
||||||
|
|
||||||
/* GET home page. */
|
/* GET home page. */
|
||||||
router.get('/', function(req, res, next) {
|
router.get('/', async function(req, res, next) {
|
||||||
res.render('index', { title: 'Express' });
|
const clientConfig = await getFullConfig();
|
||||||
|
res.render('index', { 'node': clientConfig });
|
||||||
});
|
});
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
|
|||||||
177
Client/setup.sh
177
Client/setup.sh
@@ -5,21 +5,81 @@ if [ "$EUID" -ne 0 ]
|
|||||||
then echo "Please run as root"
|
then echo "Please run as root"
|
||||||
exit
|
exit
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Prompt the user for reboot confirmation
|
||||||
|
read -p "This script will install all required components for the DRB client. Are you okay with rebooting afterward? If not, you will have to reboot later before running the applications to finish the installation. (Reboot?: y/n): " confirm
|
||||||
|
|
||||||
|
# Convert user input to lowercase for case-insensitive comparison
|
||||||
|
confirm="${confirm,,}"
|
||||||
|
|
||||||
|
if [[ "$confirm" != "y" && "$confirm" != "yes" ]]; then
|
||||||
|
echo "Script will not reboot."
|
||||||
|
should_reboot=false
|
||||||
|
else
|
||||||
|
echo "Script will reboot after completion."
|
||||||
|
should_reboot=true
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "----- Starting Radio Node Client Install Script -----"
|
||||||
|
|
||||||
SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
|
SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
|
||||||
|
|
||||||
ls -ld $SCRIPT_DIR | awk '{print $3}' >> ./config/installerName
|
# Copy the example env file
|
||||||
|
cp .env.example .env
|
||||||
|
|
||||||
# Setup user for service
|
# Copy the radio config example file
|
||||||
useradd -M RadioNode
|
cp config/radioPresets.json.EXAMPLE config/radioPresets.json
|
||||||
usermod -s -L RadioNode
|
|
||||||
|
|
||||||
# Change the ownership of the directory to the service user
|
echo "----- Collecting Setup Information -----"
|
||||||
chown RadioNode -R $SCRIPT_DIR
|
|
||||||
|
|
||||||
# Check for updates
|
# Ask the user for input and store in variables
|
||||||
apt-get update
|
echo " \\Client Config"
|
||||||
|
read -p " Enter Node Name: " nodeName
|
||||||
|
read -p " Enter Node IP: " nodeIP
|
||||||
|
read -p " Enter Node Port: " nodePort
|
||||||
|
read -p " Enter Node Location: " nodeLocation
|
||||||
|
read -p " Enter Audio Device ID: " audioDeviceID
|
||||||
|
echo " \\Server Config"
|
||||||
|
read -p " Enter Server IP (leave blank if using hostname): " serverIP
|
||||||
|
if [ -z "$serverIP" ]; then
|
||||||
|
read -p " Enter Server Hostname: " serverHostname
|
||||||
|
fi
|
||||||
|
read -p " Enter Server Port: " serverPort
|
||||||
|
|
||||||
|
# Update the values in the env file using sed
|
||||||
|
sed -i "s/^AUDIO_DEVICE_ID=\".*\"$/AUDIO_DEVICE_ID=\"$audioDeviceID\"/" .env
|
||||||
|
sed -i "s/^CLIENT_NAME=\".*\"$/CLIENT_NAME=\"$nodeName\"/" .env
|
||||||
|
sed -i "s/^CLIENT_IP=\".*\"$/CLIENT_IP=\"$nodeIP\"/" .env
|
||||||
|
sed -i "s/^CLIENT_PORT=.*$/CLIENT_PORT=$nodePort/" .env
|
||||||
|
sed -i "s/^CLIENT_LOCATION=\".*\"$/CLIENT_LOCATION=\"$nodeLocation\"/" .env
|
||||||
|
if [ -z "$serverIP" ]; then
|
||||||
|
sed -i "s/^SERVER_HOSTNAME=\".*\"$/SERVER_HOSTNAME=\"$serverHostname\"/" .env
|
||||||
|
else
|
||||||
|
sed -i "s/^SERVER_IP=\".*\"$/SERVER_IP=\"$serverIP\"/" .env
|
||||||
|
fi
|
||||||
|
sed -i "s/^SERVER_PORT=\".*\"$/SERVER_PORT=\"$serverPort\"/" .env
|
||||||
|
echo "----- Config file has been updated -----"
|
||||||
|
|
||||||
|
# Display the updated values
|
||||||
|
echo "----- Start of Config File -----"
|
||||||
|
cat .env
|
||||||
|
echo "----- End of Config File -----"
|
||||||
|
|
||||||
|
echo "----- Getting Dependencies -----"
|
||||||
|
|
||||||
# Install Node Repo
|
# Install Node Repo
|
||||||
|
# Get the CPU architecture
|
||||||
|
cpu_arch=$(uname -m)
|
||||||
|
|
||||||
|
# Print the CPU architecture for verification
|
||||||
|
echo "Detected CPU Architecture: $cpu_arch"
|
||||||
|
|
||||||
|
# Check if the architecture is ARMv6
|
||||||
|
if [[ "$cpu_arch" == "armv6"* ]]; then
|
||||||
|
echo "----- CPU Architecture is ARMv6 or compatible. -----"
|
||||||
|
echo "----- CPU Architectre is not compatible with dependencies of this project, please use a newer CPU architecture -----"
|
||||||
|
exit
|
||||||
|
|
||||||
curl -fsSL https://deb.nodesource.com/setup_current.x | sudo -E bash -
|
curl -fsSL https://deb.nodesource.com/setup_current.x | sudo -E bash -
|
||||||
|
|
||||||
# Update the system
|
# Update the system
|
||||||
@@ -27,16 +87,41 @@ apt-get update
|
|||||||
apt-get upgrade -y
|
apt-get upgrade -y
|
||||||
|
|
||||||
# Install the necessary packages
|
# Install the necessary packages
|
||||||
apt-get install -y nodejs portaudio19-dev libportaudio2 libpulse-dev pulseaudio apulse python3 python3-pip
|
apt-get install -y nodejs portaudio19-dev libportaudio2 libpulse-dev pulseaudio apulse python3 python3-pip git
|
||||||
|
|
||||||
# Ensure pulse audio is running
|
|
||||||
pulseaudio
|
|
||||||
|
|
||||||
# Install the node packages from the project
|
# Install the node packages from the project
|
||||||
npm i
|
npm i
|
||||||
|
|
||||||
# Install the python packages needed for the bot
|
# Install the python packages needed for the bot
|
||||||
pip install -r
|
pip install -r ./pdab/requirements.txt
|
||||||
|
|
||||||
|
echo "----- Setting up Pulse Audio -----"
|
||||||
|
|
||||||
|
# Ensure pulse audio is running as system so the service can see the audio device
|
||||||
|
systemctl --global disable pulseaudio.service pulseaudio.socket
|
||||||
|
|
||||||
|
# Update the PulseAudio config to disable autospawning
|
||||||
|
sed -i 's/autospawn = .*$/autospawn = no/' /etc/pulse/client.conf
|
||||||
|
|
||||||
|
# Add the system PulseAudio service
|
||||||
|
echo "[Unit]
|
||||||
|
Description=PulseAudio system server
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=notify
|
||||||
|
ExecStart=pulseaudio --daemonize=no --system --realtime --log-target=journal
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target" >> /etc/systemd/system/PulseAudio.service
|
||||||
|
|
||||||
|
# Add the root user to the pulse-access group
|
||||||
|
usermod -aG pulse-access root
|
||||||
|
usermod -aG pulse-access pi
|
||||||
|
|
||||||
|
# Enable the PulseAudio service
|
||||||
|
systemctl enable PulseAudio.service
|
||||||
|
|
||||||
|
echo "----- Setting up Radio Node Service -----"
|
||||||
|
|
||||||
# Setup bot service
|
# Setup bot service
|
||||||
echo "[Unit]
|
echo "[Unit]
|
||||||
@@ -44,32 +129,80 @@ Description=Radio Node Service
|
|||||||
After=network.target
|
After=network.target
|
||||||
|
|
||||||
[Service]
|
[Service]
|
||||||
WorkingDirectory="$SCRIPT_DIR"
|
WorkingDirectory=$SCRIPT_DIR/
|
||||||
ExecStart=/usr/bin/node .
|
ExecStart=/usr/bin/node .
|
||||||
Restart=always
|
Restart=always
|
||||||
RestartDelay=10
|
RestartDelay=10
|
||||||
User=RadioNode
|
Environment=\"DEBUG='client:*'\"
|
||||||
Environment=DEBUG='client:*'
|
|
||||||
|
|
||||||
[Install]
|
[Install]
|
||||||
WantedBy=multi-user.target" >> /etc/systemd/system/RadioNode.service
|
WantedBy=multi-user.target" >> /etc/systemd/system/RadioNode.service
|
||||||
|
|
||||||
|
# Enable the Radio Node service
|
||||||
|
systemctl enable RadioNode.service
|
||||||
|
|
||||||
|
echo "----- Setting up Radio Node Update Service -----"
|
||||||
|
|
||||||
# Setup bot update service
|
# Setup bot update service
|
||||||
echo "[Unit]
|
echo "[Unit]
|
||||||
Description=Radio Node Updater Service
|
Description=Radio Node Updater Service
|
||||||
After=network.target
|
After=network.target
|
||||||
|
|
||||||
[Service]
|
[Service]
|
||||||
WorkingDirectory="$SCRIPT_DIR"
|
WorkingDirectory=$SCRIPT_DIR/
|
||||||
ExecStart=/usr/bin/bash update.sh
|
ExecStart=/usr/bin/bash update.sh
|
||||||
Restart=on-failure
|
Restart=on-failure
|
||||||
User=RadioNode
|
|
||||||
|
|
||||||
[Install]
|
[Install]
|
||||||
WantedBy=multi-user.target" >> /etc/systemd/system/RadioNodeUpdater.service
|
WantedBy=multi-user.target" >> /etc/systemd/system/RadioNodeUpdater.service
|
||||||
|
|
||||||
# Enable the service
|
# Install OP25
|
||||||
systemctl enable RadioNode.service
|
echo "----- Installing OP25 from Source -----"
|
||||||
|
# Clone the OP25 Git
|
||||||
|
cd /opt/
|
||||||
|
git clone https://github.com/boatbod/op25.git
|
||||||
|
cd op25
|
||||||
|
|
||||||
# Start the service
|
# Run the OP25 install script
|
||||||
systemctl start RadioNode.service
|
bash ./install.sh
|
||||||
|
|
||||||
|
# Create the config file for the client or user to update later
|
||||||
|
cp /opt/op25/op25/gr-op25_repeater/apps/p25_rtl_example.json /opt/op25/op25/gr-op25_repeater/apps/radioNodeOP25Config.json
|
||||||
|
|
||||||
|
# Create the OP25 service
|
||||||
|
echo "[Unit]
|
||||||
|
Description=OP25 Service
|
||||||
|
After=network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
WorkingDirectory=/opt/op25/op25/gr-op25_repeater/apps
|
||||||
|
ExecStart=./multi_rx.py -c radioNodeOP25Config.json
|
||||||
|
Restart=always
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target" >> /etc/systemd/system/OP25.service
|
||||||
|
|
||||||
|
echo "----- OP25 Setup Complete -----"
|
||||||
|
|
||||||
|
# Enable the OP25 service, don't start it though as the user needs to config
|
||||||
|
systemctl enable OP25.service
|
||||||
|
echo "----- OP25 Enabled; Please ensure to update the configuration and start the service -----"
|
||||||
|
|
||||||
|
# Move back to the directory that the user started in (might not be needed?)
|
||||||
|
cd $SCRIPT_DIR
|
||||||
|
|
||||||
|
echo "----- Setup Complete! -----"
|
||||||
|
|
||||||
|
# Reboot if the user confirmed earlier
|
||||||
|
if [ "$should_reboot" = true ]; then
|
||||||
|
echo "To configure the app, please go to http://$nodeIP:$nodePort"
|
||||||
|
echo "Thank you for joining the network!"
|
||||||
|
# Prompt user to press any key before rebooting
|
||||||
|
read -rsp $'System will now reboot, press any key to continue or Ctrl+C to cancel...\n' -n1 key
|
||||||
|
echo "Rebooting..."
|
||||||
|
reboot
|
||||||
|
else
|
||||||
|
echo "To configure the app, please go to http://$nodeIP:$nodePort"
|
||||||
|
echo "Thank you for joining the network!"
|
||||||
|
echo "Please restart your device to complete the installation"
|
||||||
|
fi
|
||||||
|
|||||||
@@ -17,10 +17,12 @@ echo "<!-- UPDATING ---!>"
|
|||||||
# Stop any running service
|
# Stop any running service
|
||||||
systemctl stop RadioNode
|
systemctl stop RadioNode
|
||||||
|
|
||||||
# Update the git Repo
|
# Get the owner of the current working directory
|
||||||
installUser=$(cat ./config/installerName)
|
cwd_owner=$(stat -c '%U' .)
|
||||||
sudo su -l $installUser -c 'git fetch -a -p'
|
|
||||||
sudo su -l $installUser -c 'git pull'
|
# Update the git Repo as the owner of the current working directory
|
||||||
|
sudo su -l $cwd_owner -c 'git fetch'
|
||||||
|
sudo su -l $cwd_owner -c 'git pull'
|
||||||
|
|
||||||
# Install any new libraries
|
# Install any new libraries
|
||||||
npm i
|
npm i
|
||||||
|
|||||||
@@ -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();
|
||||||
@@ -34,3 +36,7 @@ function getDeviceName(){
|
|||||||
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()});
|
||||||
|
}
|
||||||
@@ -9,11 +9,11 @@ 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 (!this.hostname && 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_IP) this.hostname = process.env.SERVER_IP;
|
||||||
if (!this.hostname) throw new Error("No server hostname / IP was given when creating a request");
|
if (!this.hostname) throw new Error("No server hostname / IP was given when creating a request");
|
||||||
this.path = path;
|
this.path = path;
|
||||||
this.port = port ?? process.env.SERVER_PORT;
|
this.port = port ?? process.env.SERVER_PORT;
|
||||||
|
|||||||
@@ -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,66 @@ 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 {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
|
* @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
|
* 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
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ function writePresets(presets, callback = undefined) {
|
|||||||
// Error checking
|
// Error checking
|
||||||
if (err) throw err;
|
if (err) throw err;
|
||||||
log.DEBUG("Write Complete");
|
log.DEBUG("Write Complete");
|
||||||
if (callback) callback()
|
if (callback) callback(); else return
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -72,7 +72,8 @@ function convertFrequencyToHertz(frequency){
|
|||||||
exports.getPresets = function getPresets() {
|
exports.getPresets = function getPresets() {
|
||||||
const presetDir = path.resolve("./config/radioPresets.json");
|
const presetDir = path.resolve("./config/radioPresets.json");
|
||||||
log.DEBUG(`Getting presets from directory: '${presetDir}'`);
|
log.DEBUG(`Getting presets from directory: '${presetDir}'`);
|
||||||
return JSON.parse(fs.readFileSync(presetDir));
|
if (fs.existsSync(presetDir)) return JSON.parse(fs.readFileSync(presetDir));
|
||||||
|
else return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,169 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html lang="en" data-bs-theme="auto">
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<title><%= title %></title>
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<link rel='stylesheet' href='/stylesheets/style.css' />
|
<title>'<%=node.name%>' - Configuration</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>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<h1><%= title %></h1>
|
<%- include('partials/navbar.ejs') %>
|
||||||
<p>Welcome to <%= title %></p>
|
<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="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">
|
||||||
|
<div class="col-md-12 pt-2">
|
||||||
|
<label class="small mb-1" for="nodeStatus">Online Status:</label>
|
||||||
|
<span class="badge badge-soft-success mb-0 align-middle fs-6" id="nodeStatus">Online</span>
|
||||||
|
|
||||||
|
<br>
|
||||||
|
<div class="py-2"></div>
|
||||||
|
<!-- Join Server button-->
|
||||||
|
<a type="button" class="btn btn-info text-white" data-bs-toggle="modal" data-bs-target="#joinModal"
|
||||||
|
href="#">Join Server</a>
|
||||||
|
<!-- Leave Server button -->
|
||||||
|
<a type="button" class="btn btn-danger" href="#" onclick="leaveServer()">Leave Server</a>
|
||||||
|
<!-- Checkin with client button -->
|
||||||
|
<a type="button" class="btn btn-secondary" href="#" onclick="sendNodeHeartbeat('<%=node.id%>')">Check-in
|
||||||
|
with Node</a>
|
||||||
|
<!-- Update Client button -->
|
||||||
|
<a type="button" class="btn btn-warning disabled" href="#"
|
||||||
|
onclick="requestNodeUpdate('<%=node.id%>')">Update Node</a>
|
||||||
|
</div>
|
||||||
|
<hr>
|
||||||
|
<form>
|
||||||
|
<div class="row gx-3 mb-3">
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<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> </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 class="table-link text-danger label" onclick="removeSystem('<%=system%>')">
|
||||||
|
<i class="bi bi-trash"></i>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<% // Update system modal %>
|
||||||
|
<%- include("partials/modifySystemModal.ejs", {'system': system, 'frequencies' :
|
||||||
|
node.nearbySystems[system].frequencies, 'mode' : node.nearbySystems[system].mode}) %>
|
||||||
|
<% } %>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Save changes button-->
|
||||||
|
<button class="btn btn-primary" type="button" onclick="saveNodeDetails()">Save changes</button>
|
||||||
|
<!-- Button trigger modal -->
|
||||||
|
<button type="button" class="btn btn-primary float-right" data-bs-toggle="modal"
|
||||||
|
data-bs-target="#updateSystemModal_New_System">Add New System</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<% // new System Modal %>
|
||||||
|
<%- include("partials/modifySystemModal.ejs", {'system': "New System" , 'frequencies' : [], 'mode' : '' }) %>
|
||||||
|
<% // Join Server Modal %>
|
||||||
|
<%- include("partials/joinModal.ejs", {'node': node}) %>
|
||||||
</body>
|
</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>
|
||||||
|
|
||||||
|
<script src="/res/js/node.js"></script>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
44
Client/views/partials/joinModal.ejs
Normal file
44
Client/views/partials/joinModal.ejs
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
<div class="modal fade" id="joinModal" tabindex="-1" aria-labelledby="joinModal" 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="joinModal">Join Node <%=node.id%> to a Discord Server</h1>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="container">
|
||||||
|
<div class="card mb-4">
|
||||||
|
<div class="card-body">
|
||||||
|
<form>
|
||||||
|
<div class="row gx-3 mb-3">
|
||||||
|
<div class="col-md-12">
|
||||||
|
<label class="small mb-1" for="inputDiscordClientId">Discord Client ID:</label>
|
||||||
|
<input class="form-control" id="inputDiscordClientId" type="text" value="" required></input>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row gx-3 mb-3">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="small mb-1" for="inputDiscordChannelId">Discord Channel ID:</label>
|
||||||
|
<input class="form-control" id="inputDiscordChannelId" type="text" value="" required></input>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="small mb-1" for="selectRadioPreset">Selected Preset:</label>
|
||||||
|
<select class="custom-select" id="selectRadioPreset">
|
||||||
|
<% for(const system in node.nearbySystems) { %>
|
||||||
|
<option value="<%=system%>"><%=system%></option>
|
||||||
|
<% } %>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
||||||
|
<button type="button" class="btn btn-primary" onclick="joinServer()">Join</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
61
Client/views/partials/modifySystemModal.ejs
Normal file
61
Client/views/partials/modifySystemModal.ejs
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
<div class="modal fade" id="updateSystemModal_<%=system.replaceAll(" ", "_")%>" tabindex="-1" aria-labelledby="updateSystemModal_<%=system.replaceAll(" ", "_")%>"
|
||||||
|
aria-hidden="true" data-bs-backdrop="static" data-bs-keyboard="false">
|
||||||
|
<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(" ", "_")%>"><%if (!system == "New System") {%>Update<%} else {%>Add a<%}%> <%=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">
|
||||||
|
<label class="small mb-1 fs-6" for="systemName">System Name</label>
|
||||||
|
<input class="form-control" id="<%=system%>_systemName" type="text" value="<%if (system != "New System") {%><%= system %><%} else {%>Local Radio System<%}%>"></input>
|
||||||
|
</div>
|
||||||
|
<div class="row gx-3 mb-3" id="frequencyRow_<%=system.replaceAll(" ", "_")%>">
|
||||||
|
<label class="small mb-1 fs-6" for="systemFreq">Frequencies</label>
|
||||||
|
<% for(const frequency of frequencies) { %>
|
||||||
|
<div class="col-md-6 mb-1" id="<%=system%>_systemFreqRow_<%=frequency%>">
|
||||||
|
<div class="row px-1">
|
||||||
|
<div class="col-10">
|
||||||
|
<input class="form-control" id="<%=system%>_systemFreq_<%=frequency%>" type="text" value="<%= frequency %>"></input>
|
||||||
|
</div>
|
||||||
|
<div class="col-2">
|
||||||
|
<a class="align-middle float-left" href="#" onclick="removeFrequencyInput('<%=system%>_systemFreqRow_<%=frequency%>')"><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="<%=system%>_systemMode">Mode</label>
|
||||||
|
<br>
|
||||||
|
<select class="custom-select" id="<%=system%>_systemMode">
|
||||||
|
<option value="<%= mode ?? 'select' %>" selected><span class="text-uppercase"><%= mode ?? 'Select' %></span></option>
|
||||||
|
<% if(mode == "p25") { %>
|
||||||
|
<option value="nbfm">NBFM</option>
|
||||||
|
<% } else if (mode == "nbfm") { %>
|
||||||
|
<option value="p25">P25</option>
|
||||||
|
<% } else { %>
|
||||||
|
<option value="nbfm">NBFM</option>
|
||||||
|
<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" onclick="location.reload()">Close</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>
|
||||||
42
Client/views/partials/navbar.ejs
Normal file
42
Client/views/partials/navbar.ejs
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
<nav class="navbar fixed-top 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" id="navbar-notification-bell" onclick="showStoredToasts()"><i class="bi bi-bell-fill"></i></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>
|
||||||
@@ -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 { filterAutocompleteValues } = require("../utilities/utils");
|
const { filterAutocompleteValues, filterPresetsAvailable } = require("../utilities/utils");
|
||||||
const { getOnlineNodes, getAllConnections } = require("../utilities/mysqlHandler");
|
const { getOnlineNodes, getAllConnections } = require("../utilities/mysqlHandler");
|
||||||
const { joinServerWrapper } = require("../controllers/adminController");
|
const { joinServerWrapper } = require("../controllers/adminController");
|
||||||
|
|
||||||
@@ -28,18 +28,7 @@ module.exports = {
|
|||||||
recordResolve(nodeRows);
|
recordResolve(nodeRows);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
log.DEBUG("Node objects: ", nodeObjects);
|
const options = await filterPresetsAvailable(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);
|
|
||||||
|
|
||||||
// Filter the results to what the user is entering
|
// Filter the results to what the user is entering
|
||||||
filterAutocompleteValues(interaction, options);
|
filterAutocompleteValues(interaction, options);
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ const { sendHttpRequest, requestOptions } = require("../utilities/httpRequests.j
|
|||||||
const { nodeObject } = require("../utilities/recordHelper.js");
|
const { nodeObject } = require("../utilities/recordHelper.js");
|
||||||
|
|
||||||
const refreshInterval = process.env.NODE_MONITOR_REFRESH_INTERVAL ?? 1200000;
|
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
|
* Check in with a singular node, mark it offline if it's offline and
|
||||||
@@ -19,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");
|
||||||
@@ -110,20 +111,129 @@ exports.newNode = async (req, res) => {
|
|||||||
* @param {*} res Defualt express res 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.params.id) return res.status(400).json("No id specified");
|
||||||
getNodeInfoFromId(req.query.id, (nodeInfo) => {
|
getNodeInfoFromId(req.params.id, (nodeInfo) => {
|
||||||
res.status(200).json(nodeInfo);
|
res.status(200).json(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
|
/** Updates the information received from the client based on ID
|
||||||
*
|
*
|
||||||
* @param {*} req Default express req from router
|
* @param {*} req Default express req from router
|
||||||
* @param {*} res Defualt express res from router
|
* @param {*} res Defualt express res from router
|
||||||
*/
|
*/
|
||||||
exports.nodeCheckIn = async (req, res) => {
|
exports.updateExistingNode = async = (req, res) => {
|
||||||
if (!req.body.id) return res.status(400).json("No id specified");
|
if (!req.params.nodeId) return res.status(400).json("No id specified");
|
||||||
getNodeInfoFromId(req.body.id, (nodeInfo) => {
|
getNodeInfoFromId(req.params.nodeId, (nodeInfo) => {
|
||||||
let checkInObject = {};
|
let checkInObject = {};
|
||||||
// Convert the online status to a boolean to be worked with
|
// Convert the online status to a boolean to be worked with
|
||||||
log.DEBUG("REQ Body: ", req.body);
|
log.DEBUG("REQ Body: ", req.body);
|
||||||
@@ -163,25 +273,46 @@ exports.nodeCheckIn = async (req, res) => {
|
|||||||
// If no changes are made tell the client
|
// 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);
|
checkInObject = new nodeObject(checkInObject);
|
||||||
|
|
||||||
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) => {
|
||||||
|
await checkInWithNode(newNode);
|
||||||
return res.status(201).json({ "updatedKeys": newNode });
|
return res.status(201).json({ "updatedKeys": newNode });
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
updateNodeInfo(checkInObject, () => {
|
updateNodeInfo(checkInObject, async () => {
|
||||||
|
await checkInWithNode(nodeInfo);
|
||||||
return res.status(202).json({ "updatedKeys": checkInObject });
|
return res.status(202).json({ "updatedKeys": checkInObject });
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Allows the bots to check in and get any updates from the server
|
||||||
|
*
|
||||||
|
* @param {*} req Default express req from router
|
||||||
|
* @param {*} res Defualt express res from router
|
||||||
|
*/
|
||||||
|
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
|
* Requests a specific node to check in with the server, if it's online
|
||||||
*
|
*
|
||||||
@@ -193,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);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -82,6 +82,7 @@ a {
|
|||||||
position: relative;
|
position: relative;
|
||||||
margin-bottom: 30px;
|
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);
|
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 {
|
.info-card .card-statistic .card-icon-large .bi {
|
||||||
@@ -138,7 +139,14 @@ a {
|
|||||||
|
|
||||||
/* Global Section */
|
/* Global Section */
|
||||||
.sidebar-container {
|
.sidebar-container {
|
||||||
min-height: 94.2vh;
|
min-height: 95vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar {
|
||||||
|
position: fixed;
|
||||||
|
top: 5vh;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* User table section */
|
/* User table section */
|
||||||
|
|||||||
@@ -1,4 +1,121 @@
|
|||||||
function addFrequencyInput(system){
|
$(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
|
// Create new input
|
||||||
var icon = document.createElement('i');
|
var icon = document.createElement('i');
|
||||||
icon.classList.add('bi');
|
icon.classList.add('bi');
|
||||||
@@ -9,6 +126,7 @@ function addFrequencyInput(system){
|
|||||||
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.appendChild(icon);
|
remove.appendChild(icon);
|
||||||
|
|
||||||
var childColRemoveIcon = document.createElement('div');
|
var childColRemoveIcon = document.createElement('div');
|
||||||
@@ -17,7 +135,7 @@ function addFrequencyInput(system){
|
|||||||
|
|
||||||
var input = document.createElement('input');
|
var input = document.createElement('input');
|
||||||
input.classList.add('form-control');
|
input.classList.add('form-control');
|
||||||
input.id = 'nodeFreq';
|
input.id = `${system}_systemFreq_${inputId}`;
|
||||||
input.type = 'text';
|
input.type = 'text';
|
||||||
|
|
||||||
var childColInput = document.createElement('div');
|
var childColInput = document.createElement('div');
|
||||||
@@ -33,24 +151,36 @@ function addFrequencyInput(system){
|
|||||||
var colParent = document.createElement('div');
|
var colParent = document.createElement('div');
|
||||||
colParent.classList.add("col-md-6");
|
colParent.classList.add("col-md-6");
|
||||||
colParent.classList.add("mb-1");
|
colParent.classList.add("mb-1");
|
||||||
|
colParent.id = `${system}_systemFreqRow_${inputId}`
|
||||||
colParent.appendChild(childRow);
|
colParent.appendChild(childRow);
|
||||||
|
|
||||||
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');
|
||||||
@@ -58,7 +188,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));
|
||||||
@@ -69,26 +198,35 @@ function createToast(notificationMessage){
|
|||||||
|
|
||||||
const wrapperDiv = document.createElement('div');
|
const wrapperDiv = document.createElement('div');
|
||||||
wrapperDiv.classList.add('toast');
|
wrapperDiv.classList.add('toast');
|
||||||
|
//wrapperDiv.classList.add('position-fixed');
|
||||||
|
wrapperDiv.id = `${time.toISOString()}-toast`;
|
||||||
wrapperDiv.role = 'alert';
|
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);
|
||||||
|
if (showNow) {
|
||||||
|
const toastElement = bootstrap.Toast.getOrCreateInstance(document.getElementById(`${time.toISOString()}-toast`));
|
||||||
|
toastElement.show();
|
||||||
|
}
|
||||||
|
|
||||||
$('.toast').toast('show');
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
function sendNodeHeartbeat(nodeId) {
|
function sendNodeHeartbeat(nodeId) {
|
||||||
const Http = new XMLHttpRequest();
|
const Http = new XMLHttpRequest();
|
||||||
const url='/nodes/'+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 });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -117,8 +255,8 @@ 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');
|
location.reload();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -138,6 +276,132 @@ 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 });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
@@ -2,7 +2,11 @@
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
Overview here
|
The server application acts as the central hub within Discord, providing various functionalities and serving as the main point of communication for the clients. Some of the key features and responsibilities of the server include:
|
||||||
|
|
||||||
|
- **RSS Feed Updates**: The server periodically updates text channels with RSS feed updates, keeping users informed about the latest news or information.
|
||||||
|
- **Server Management Functions / User Requests**: The server includes management functions that allow administrators to control and configure various aspects of the server environment. Users can interact with the server through Discord commands, which range from requesting specific radio presets to updating RSS feeds.
|
||||||
|
- **API and Web Front End**: The server exposes an API and web front end, providing an interface to view and control all the online clients. This allows users to monitor and manage the available radio presets, as well as perform various administrative tasks.
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
|
|||||||
@@ -1,32 +1,25 @@
|
|||||||
var express = require('express');
|
var express = require('express');
|
||||||
var router = express.Router();
|
var router = express.Router();
|
||||||
|
|
||||||
const { getAllNodes, getNodeInfoFromId } = require("../utilities/mysqlHandler");
|
const { getAllNodes, getNodeInfoFromId, getAllConnections } = require("../utilities/mysqlHandler");
|
||||||
|
const { filterPresetsAvailable } = require("../utilities/utils");
|
||||||
|
|
||||||
/* GET home page. */
|
/* GET home page. */
|
||||||
router.get('/', (req, res) => {
|
router.get('/', async (req, res) => {
|
||||||
//var sources = libCore.getSources();
|
var nodes = await new Promise((recordResolve, recordReject) => {
|
||||||
return res.render('index');
|
getAllNodes((nodeRows) => {
|
||||||
|
recordResolve(nodeRows);
|
||||||
var htmlOutput = "";
|
|
||||||
|
|
||||||
sources.forEach(source => {
|
|
||||||
htmlOutput += `
|
|
||||||
<div style='margin-bottom:15px;'>
|
|
||||||
|
|
||||||
<div> Title: ${source.title} </div>
|
|
||||||
<div> Link: ${source.link} </div>
|
|
||||||
<div> category: ${source.category} </div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<hr />
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
`
|
|
||||||
});
|
});
|
||||||
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. */
|
/* GET node controller page. */
|
||||||
@@ -38,7 +31,7 @@ router.get('/controller', async (req, res) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
//var sources = libCore.getSources();
|
//var sources = libCore.getSources();
|
||||||
return res.render('controller', {'nodes' : nodes});
|
return res.render('controller', { 'nodes': nodes, 'page': 'controller' });
|
||||||
});
|
});
|
||||||
|
|
||||||
/* GET individual node page. */
|
/* GET individual node page. */
|
||||||
@@ -46,7 +39,7 @@ router.get('/node/:id', async (req, res) => {
|
|||||||
var node = await getNodeInfoFromId(req.params.id);
|
var node = await getNodeInfoFromId(req.params.id);
|
||||||
|
|
||||||
//var sources = libCore.getSources();
|
//var sources = libCore.getSources();
|
||||||
return res.render('node', {'node' : node});
|
return res.render('node', { 'node': node, 'page': 'node' });
|
||||||
});
|
});
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
|
|||||||
@@ -5,6 +5,22 @@ const nodesController = require('../controllers/nodesController');
|
|||||||
/* GET nodes the server knows */
|
/* GET nodes the server knows */
|
||||||
router.get('/', nodesController.listAllNodes);
|
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
|
// TODO Need to authenticate this request
|
||||||
/* POST a new node to the server
|
/* POST a new node to the server
|
||||||
*
|
*
|
||||||
@@ -21,14 +37,10 @@ router.get('/', nodesController.listAllNodes);
|
|||||||
router.post('/newNode', nodesController.newNode);
|
router.post('/newNode', nodesController.newNode);
|
||||||
|
|
||||||
// TODO Need to authenticate this request
|
// TODO Need to authenticate this request
|
||||||
/* GET the information the server has on a particular node */
|
// Client checkin with the server to update client information
|
||||||
router.get('/nodeInfo', nodesController.getNodeInfo);
|
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);
|
|
||||||
|
|
||||||
// Request a node to check in with the server
|
// Request a node to check in with the server
|
||||||
router.get('/:nodeId', nodesController.requestNodeCheckIn);
|
router.get('/nodeCheckIn/:nodeId', nodesController.requestNodeCheckIn);
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
|
|||||||
@@ -139,3 +139,25 @@ exports.filterAutocompleteValues = async (interaction, options) => {
|
|||||||
filtered.map(option => ({ name: option, value: option })),
|
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;
|
||||||
|
}
|
||||||
@@ -1,133 +1,64 @@
|
|||||||
<%- include('partials/htmlHead.ejs') %>
|
<%- include('partials/htmlHead.ejs', {'page': page}) %>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
|
<div class="row row-cols-1 row-cols-md-2 row-cols-xl-4 mt-2">
|
||||||
|
<%- 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'
|
||||||
|
}) %>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="my-4">
|
||||||
|
<p><h3><b>Current Connections</b></h3></p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
<div class="row row-cols-1 row-cols-md-2 row-cols-xl-4">
|
<div class="row row-cols-1 row-cols-md-2 row-cols-xl-4">
|
||||||
<div class="col-xl-3 col-lg-6">
|
<% for(const conn of connections) { %>
|
||||||
<div class="info-card l-bg-cherry">
|
<%- include('partials/connectionCard.ejs', {'connection': conn}) %>
|
||||||
<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>
|
||||||
|
|
||||||
<div class="col-xl-3 col-lg-6">
|
<div class="my-4">
|
||||||
<div class="info-card l-bg-blue-dark">
|
<p><h3><b>Online Nodes</b></h3></p>
|
||||||
<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>
|
||||||
|
|
||||||
<div class="col-xl-3 col-sm-6">
|
<hr>
|
||||||
<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="row row-cols-1 row-cols-md-2 row-cols-xl-4">
|
||||||
<div class="card node-card">
|
<% for(const node of nodes.filter(node => node.online)) { %>
|
||||||
<div class="card-body">
|
<%- include('partials/nodeCard.ejs', {'node': node}) %>
|
||||||
<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>
|
||||||
<div class="d-flex align-items-center">
|
<div class="my-4">
|
||||||
<div><img src="https://bootdey.com/img/Content/avatar/avatar5.png" alt=""
|
<p><h3><b>Offline Nodes</b></h3></p>
|
||||||
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>
|
<hr>
|
||||||
<div class="mt-3 pt-1">
|
<div class="row row-cols-1 row-cols-md-2 row-cols-xl-4">
|
||||||
<p class="text-muted mb-0"><i
|
<% for(const node of nodes.filter(node => node.online == false)) { %>
|
||||||
class="mdi mdi-phone font-size-15 align-middle pe-2 text-primary"></i> 087 6321 3235
|
<%- include('partials/nodeCard.ejs', {'node': node}) %>
|
||||||
</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>
|
||||||
</div>
|
</div>
|
||||||
<%- include('partials/bodyEnd.ejs') %>
|
<%- include('partials/bodyEnd.ejs') %>
|
||||||
|
|
||||||
<%- include('partials/htmlFooter.ejs') %>
|
<%- include('partials/htmlFooter.ejs') %>
|
||||||
@@ -9,28 +9,29 @@
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<form>
|
<div class="col-md-12 pt-2">
|
||||||
<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>
|
<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>
|
<% 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>
|
<% } else {%> <span class="badge badge-soft-danger mb-0 align-middle fs-6">Offline</span>
|
||||||
<% } %>
|
<% } %>
|
||||||
<hr>
|
<br>
|
||||||
|
<div class="py-2"></div>
|
||||||
<!-- Join Server button-->
|
<!-- Join Server button-->
|
||||||
<a type="button" class="btn btn-info <% if(!node.online) { %>disabled<% } %>" data-bs-toggle="modal" data-bs-target="#joinModal" href="#">Join Server</a>
|
<a type="button" class="btn btn-info text-white<% if(!node.online) { %>disabled<% } %>" data-bs-toggle="modal" data-bs-target="#joinModal" href="#">Join Server</a>
|
||||||
<!-- Leave Server button -->
|
<!-- Leave Server button -->
|
||||||
<a type="button" class="btn btn-danger <% if(!node.online) { %>disabled<% } %>" href="#" onclick="leaveServer()">Leave Server</a>
|
<a type="button" class="btn btn-danger <% if(!node.online) { %>disabled<% } %>" href="#" onclick="leaveServer()">Leave Server</a>
|
||||||
<!-- 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 -->
|
||||||
|
<a type="button" class="btn btn-warning disabled" href="#" onclick="requestNodeUpdate('<%=node.id%>')">Update Node</a>
|
||||||
|
</div>
|
||||||
|
<hr>
|
||||||
|
<form>
|
||||||
|
<div class="row gx-3 mb-3">
|
||||||
|
<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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -106,13 +107,13 @@
|
|||||||
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>
|
||||||
</tr>
|
</tr>
|
||||||
<% // Update system modal %>
|
<% // Update system modal %>
|
||||||
<%- include("partials/modifySystemModal.ejs", {'system': system, 'frequencies': node.nearbySystems[system].frequencies}) %>
|
<%- include("partials/modifySystemModal.ejs", {'system': system, 'frequencies': node.nearbySystems[system].frequencies, 'mode': node.nearbySystems[system].mode}) %>
|
||||||
<% } %>
|
<% } %>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
@@ -122,34 +123,18 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- Save changes button-->
|
<!-- Save changes button-->
|
||||||
<button class="btn btn-primary <% if(!node.online) { %>disabled<% } %>" type="button">Save changes</button>
|
<button class="btn btn-primary <% if(!node.online) { %>disabled<% } %>" type="button" onclick="saveNodeDetails()">Save changes</button>
|
||||||
<!-- Button trigger modal -->
|
<!-- Button trigger modal -->
|
||||||
<button type="button" class="btn btn-primary float-right <% if(!node.online) { %>disabled<% } %>" data-bs-toggle="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>
|
data-bs-target="#updateSystemModal_New_System">Add New System</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<% // new System Modal %>
|
<% // new System Modal %>
|
||||||
<div class="modal fade" id="newSystemModal" tabindex="-1" aria-labelledby="newSystemModal" aria-hidden="true">
|
<%- include("partials/modifySystemModal.ejs", {'system': "New System", 'frequencies': [], 'mode': ''}) %>
|
||||||
<div class="modal-dialog modal-dialog-centered">
|
<% // Join Server Modal %>
|
||||||
<div class="modal-content">
|
<%- include("partials/joinModal.ejs", {'node': node}) %>
|
||||||
<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/joinModal.ejs', {'nearbySystems': node.nearbySystems}) %>
|
|
||||||
<%- include('partials/bodyEnd.ejs') %>
|
<%- include('partials/bodyEnd.ejs') %>
|
||||||
<script src="/res/js/node.js"></script>
|
|
||||||
<%- include('partials/htmlFooter.ejs') %>
|
<%- include('partials/htmlFooter.ejs') %>
|
||||||
@@ -10,3 +10,5 @@
|
|||||||
<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>
|
||||||
29
Server/views/partials/connectioncard.ejs
Normal file
29
Server/views/partials/connectioncard.ejs
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
<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">Edit</a>
|
||||||
|
<a class="dropdown-item node-action" onclick="">Send Heartbeat</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<div class="flex-1 ms-3">
|
||||||
|
<h5 class="font-size-16 mb-1"><a class="text-dark">
|
||||||
|
<%= connection.clientObject.name%>
|
||||||
|
</a></h5>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mt-3 pt-1">
|
||||||
|
<p class="text-muted mb-0">
|
||||||
|
<i class="bi bi-cpu-fill font-size-15 align-middle pe-2 text-primary"></i>
|
||||||
|
Node ID: <a href="/node/<%= connection.node.id %>"><%= connection.node.id %></a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -1,7 +1,20 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<title>Bootstrap demo</title>
|
<% switch (page) {
|
||||||
|
case "index":%>
|
||||||
|
<title>Node Dashboard</title>
|
||||||
|
<% break;
|
||||||
|
case "controller":%>
|
||||||
|
<title>Node Controller</title>
|
||||||
|
<% break;
|
||||||
|
case "node":%>
|
||||||
|
<title>Node Configuration</title>
|
||||||
|
<% break;
|
||||||
|
default:%>
|
||||||
|
<title>DRB_CnC Server</title>
|
||||||
|
<%break;
|
||||||
|
} %>
|
||||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet"
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet"
|
||||||
integrity="sha384-9ndCyUaIbzAi2FUVXJi0CjmCapSmO7SnpJef0486qhLnuZ2cdeRhO02iuK6FUUVM" crossorigin="anonymous">
|
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="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.3.0/font/bootstrap-icons.css">
|
||||||
|
|||||||
@@ -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', {'page': page}) %>
|
||||||
<body>
|
<body>
|
||||||
<div class="toast-container mx-2" id="toastZone"></div>
|
|
||||||
<%- include('navbar.ejs') %>
|
<%- include('navbar.ejs') %>
|
||||||
<%- include('sidebar.ejs') %>
|
<%- include('sidebar.ejs') %>
|
||||||
@@ -24,7 +24,7 @@
|
|||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<label class="small mb-1" for="selectRadioPreset">Selected Preset:</label>
|
<label class="small mb-1" for="selectRadioPreset">Selected Preset:</label>
|
||||||
<select class="custom-select" id="selectRadioPreset">
|
<select class="custom-select" id="selectRadioPreset">
|
||||||
<% for(const system in nearbySystems) { %>
|
<% for(const system in node.nearbySystems) { %>
|
||||||
<option value="<%=system%>"><%=system%></option>
|
<option value="<%=system%>"><%=system%></option>
|
||||||
<% } %>
|
<% } %>
|
||||||
</select>
|
</select>
|
||||||
|
|||||||
@@ -1,25 +1,29 @@
|
|||||||
<div class="modal fade" id="updateSystemModal_<%=system.replaceAll(" ", "_")%>" tabindex="-1" aria-labelledby="updateSystemModal_<%=system.replaceAll(" ", "_")%>"
|
<div class="modal fade" id="updateSystemModal_<%=system.replaceAll(" ", "_")%>" tabindex="-1" aria-labelledby="updateSystemModal_<%=system.replaceAll(" ", "_")%>"
|
||||||
aria-hidden="true">
|
aria-hidden="true" data-bs-backdrop="static" data-bs-keyboard="false">
|
||||||
<div class="modal-dialog modal-dialog-centered">
|
<div class="modal-dialog modal-dialog-centered">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h1 class="modal-title fs-5" id="updateSystemModal_<%=system.replaceAll(" ", "_")%>">Update <%=system%></h1>
|
<h1 class="modal-title fs-5" id="updateSystemModal_<%=system.replaceAll(" ", "_")%>"><%if (!system == "New System") {%>Update<%} else {%>Add a<%}%> <%=system%></h1>
|
||||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<div class="card mb-4">
|
<div class="card mb-4">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<form>
|
<form>
|
||||||
|
<div class="row gx-3 mb-3">
|
||||||
|
<label class="small mb-1 fs-6" for="systemName">System Name</label>
|
||||||
|
<input class="form-control" id="<%=system%>_systemName" type="text" value="<%if (system != "New System") {%><%= system %><%} else {%>Local Radio System<%}%>"></input>
|
||||||
|
</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="nodeFreq">Frequencies</label>
|
<label class="small mb-1 fs-6" for="systemFreq">Frequencies</label>
|
||||||
<% for(const frequency of frequencies) { %>
|
<% for(const frequency of frequencies) { %>
|
||||||
<div class="col-md-6 mb-1">
|
<div class="col-md-6 mb-1" id="<%=system%>_systemFreqRow_<%=frequency%>">
|
||||||
<div class="row px-1">
|
<div class="row px-1">
|
||||||
<div class="col-10">
|
<div class="col-10">
|
||||||
<input class="form-control" id="nodeFreq" type="text" value="<%= frequency %>"></input>
|
<input class="form-control" id="<%=system%>_systemFreq_<%=frequency%>" type="text" value="<%= frequency %>"></input>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-2">
|
<div class="col-2">
|
||||||
<a class="align-middle float-left" href="#"><i class="bi bi-x-circle text-black"></i></a>
|
<a class="align-middle float-left" href="#" onclick="removeFrequencyInput('<%=system%>_systemFreqRow_<%=frequency%>')"><i class="bi bi-x-circle text-black"></i></a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -29,13 +33,16 @@
|
|||||||
<hr>
|
<hr>
|
||||||
<div class="row gx-3 mb-3">
|
<div class="row gx-3 mb-3">
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<label class="small mb-1 fs-6" for="modeSelect">Mode</label>
|
<label class="small mb-1 fs-6" for="<%=system%>_systemMode">Mode</label>
|
||||||
<br>
|
<br>
|
||||||
<select class="custom-select" id="modeSelect">
|
<select class="custom-select" id="<%=system%>_systemMode">
|
||||||
<option value="<%= node.nearbySystems[system].mode %>" selected><span class="text-uppercase"><%= node.nearbySystems[system].mode %></span></option>
|
<option value="<%= mode ?? 'select' %>" selected><span class="text-uppercase"><%= mode ?? 'Select' %></span></option>
|
||||||
<% if(node.nearbySystems[system].mode == "p25") { %>
|
<% if(mode == "p25") { %>
|
||||||
|
<option value="nbfm">NBFM</option>
|
||||||
|
<% } else if (mode == "nbfm") { %>
|
||||||
|
<option value="p25">P25</option>
|
||||||
|
<% } else { %>
|
||||||
<option value="nbfm">NBFM</option>
|
<option value="nbfm">NBFM</option>
|
||||||
<% } else if (node.nearbySystems[system].mode == "nbfm") { %>
|
|
||||||
<option value="p25">P25</option>
|
<option value="p25">P25</option>
|
||||||
<%}%>
|
<%}%>
|
||||||
</select>
|
</select>
|
||||||
@@ -46,8 +53,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">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">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>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<nav class="navbar navbar-expand-lg bg-body-tertiary" data-bs-theme="dark">
|
<nav class="navbar fixed-top navbar-expand-lg bg-body-tertiary" data-bs-theme="dark">
|
||||||
<div class="container-fluid">
|
<div class="container-fluid">
|
||||||
<a class="navbar-brand" href="#">Node Master</a>
|
<a class="navbar-brand" href="#">Node Master</a>
|
||||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent"
|
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent"
|
||||||
@@ -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">
|
||||||
|
|||||||
@@ -12,10 +12,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="d-flex align-items-center">
|
<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">
|
<div class="flex-1 ms-3">
|
||||||
<h5 class="font-size-16 mb-1"><a href="/node/<%=node.id%>" class="text-dark">
|
<h5 class="font-size-16 mb-1"><a href="/node/<%=node.id%>" class="text-dark">
|
||||||
<%= node.name %>
|
<%= node.name %>
|
||||||
@@ -36,20 +32,15 @@
|
|||||||
<%= node.ip %>:<%= node.port %>
|
<%= node.ip %>:<%= node.port %>
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
|
<hr>
|
||||||
<p class="text-muted mb-0">
|
<p class="text-muted mb-0">
|
||||||
<div class="dropdown">
|
<i class="bi bi-broadcast-pin font-size-15 pe-2 text-primary"></i>
|
||||||
<i class="bi bi-broadcast-pin font-size-15 align-middle pe-2 text-primary"></i>
|
Nearby Systems:
|
||||||
<a class="dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">
|
<ul>
|
||||||
Nearby Systems
|
|
||||||
</a>
|
|
||||||
<ul class="dropdown-menu">
|
|
||||||
<% for(const system in node.nearbySystems){ %>
|
<% for(const system in node.nearbySystems){ %>
|
||||||
<li>
|
<li><%= system %></li>
|
||||||
<%= system %>
|
|
||||||
</li>
|
|
||||||
<% } %>
|
<% } %>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,7 +1,16 @@
|
|||||||
<div class="container-fluid">
|
<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">
|
<div class="col-auto col-md-3 col-xl-2 px-sm-2 px-0 bg-dark sidebar-container">
|
||||||
<div class="d-flex flex-column align-items-center align-items-sm-start px-3 pt-2 text-white sidebar-container">
|
<div
|
||||||
|
class="d-flex flex-column align-items-center align-items-sm-start px-3 pt-2 text-white sidebar">
|
||||||
<ul class="nav nav-pills flex-column mb-sm-auto mb-0 align-items-center align-items-sm-start" id="menu">
|
<ul class="nav nav-pills flex-column mb-sm-auto mb-0 align-items-center align-items-sm-start" id="menu">
|
||||||
<li>
|
<li>
|
||||||
<a href="/" class="nav-link px-0 align-middle">
|
<a href="/" class="nav-link px-0 align-middle">
|
||||||
@@ -10,12 +19,12 @@
|
|||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a href="/controller" class="nav-link px-0 align-middle">
|
<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>
|
<i class="fs-4 bi-grid"></i> <span class="ms-1 d-none d-sm-inline">Controller</span>
|
||||||
|
</a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<hr>
|
<hr>
|
||||||
<% /*
|
<% /* <div class="dropdown pb-4 fixed-bottom px-3">
|
||||||
<div class="dropdown pb-4 fixed-bottom px-3">
|
|
||||||
<a href="#" class="d-flex align-items-center text-white text-decoration-none dropdown-toggle"
|
<a href="#" class="d-flex align-items-center text-white text-decoration-none dropdown-toggle"
|
||||||
id="dropdownUser1" data-bs-toggle="dropdown" aria-expanded="false">
|
id="dropdownUser1" data-bs-toggle="dropdown" aria-expanded="false">
|
||||||
<img src="https://github.com/mdo.png" alt="hugenerd" width="30" height="30"
|
<img src="https://github.com/mdo.png" alt="hugenerd" width="30" height="30"
|
||||||
@@ -36,3 +45,4 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col py-3">
|
<div class="col py-3">
|
||||||
|
|
||||||
32
Server/views/partials/valueChip.ejs
Normal file
32
Server/views/partials/valueChip.ejs
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
<div class="col-xl-3 col-lg-6">
|
||||||
|
<div class="info-card l-bg-<%=bgColor%>">
|
||||||
|
<div class="card-statistic p-4">
|
||||||
|
<div class="card-icon card-icon-large"><i class="bi bi-<%=icon%>"></i></div>
|
||||||
|
<div class="mb-4">
|
||||||
|
<h5 class="card-title mb-0">
|
||||||
|
<%=title%>
|
||||||
|
</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">
|
||||||
|
<%=value%>
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<% if (progressPercent) {%>
|
||||||
|
<div class="col-4 text-right">
|
||||||
|
<span>
|
||||||
|
<%=progressPercent%>%<i class="fa fa-arrow-up"></i>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<%}%>
|
||||||
|
</div>
|
||||||
|
<% if (progressPercent) {%>
|
||||||
|
<div class="progress mt-1 " data-height="8" style="height: 8px;">
|
||||||
|
<div class="progress-bar l-bg-cyan" role="progressbar" data-width="<%=progressPercent%>%" aria-valuenow="<%=progressPercent%>"
|
||||||
|
aria-valuemin="0" aria-valuemax="100" style="width: <%=progressPercent%>%;"></div>
|
||||||
|
</div>
|
||||||
|
<%}%>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
53
readme.md
53
readme.md
@@ -1,32 +1,27 @@
|
|||||||
# Discord Radio Bot: Command & Control
|
# Project Overview
|
||||||
|
|
||||||
|
This project is a multi-layered application consisting of client and server applications. Its main purpose is to enable the use of Software-Defined Radios (SDRs) and Raspberry Pi (or similar Single Board Computers) to listen to radio frequencies in Discord voice channels. The project is designed to provide a seamless integration between the SDR hardware and the server with Discord commands.
|
||||||
|
|
||||||
|
## Server Application
|
||||||
|
|
||||||
|
The server application acts as the central hub within Discord, providing various functionalities and serving as the main point of communication for the clients. Some of the key features and responsibilities of the server include:
|
||||||
|
|
||||||
|
- **RSS Feed Updates**: The server periodically updates text channels with RSS feed updates, keeping users informed about the latest news or information.
|
||||||
|
- **Server Management Functions / User Requests**: The server includes management functions that allow administrators to control and configure various aspects of the server environment. Users can interact with the server through Discord commands, which range from requesting specific radio presets to updating RSS feeds.
|
||||||
|
- **API and Web Front End**: The server exposes an API and web front end, providing an interface to view and control all the online clients. This allows users to monitor and manage the available radio presets, as well as perform various administrative tasks.
|
||||||
|
|
||||||
|
#### [Read more about the Server](https://git.vpn.cusano.net/logan/DRB-CnC/src/branch/master/Server)
|
||||||
|
|
||||||
|
---
|
||||||
|
## Client Application
|
||||||
|
|
||||||
|
The client application communicates with the server through the provided API. Each client instance waits for join requests sent by users through Discord. Once a join request is received, the client uses the SDR application to tune into the specified radio preset. It then establishes a connection to Discord, allowing users to listen to the selected radio preset in real-time.
|
||||||
|
|
||||||
|
In addition to its interaction with the server, the client also has its own API and web application. This enables users to directly interface with the client, perform actions specific to the client application, and access relevant information about the connected SDR and radio presets.
|
||||||
|
|
||||||
|
#### [Read more about the Client](https://git.vpn.cusano.net/logan/DRB-CnC/src/branch/master/Client)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
Project overview here
|
## Troubleshooting
|
||||||
|
Check the [wiki](https://git.vpn.cusano.net/logan/DRB-CnC/wiki)
|
||||||
## Requirements Overview
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Server Requirements
|
|
||||||
#### Server: Discord Bot Requirements
|
|
||||||
### Client Requirements
|
|
||||||
#### Client: Discord Bot Requirements
|
|
||||||
|
|
||||||
## Server
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
Explanation and overview here
|
|
||||||
|
|
||||||
## Client
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
Explanation and overview here
|
|
||||||
|
|
||||||
## Discord Bot
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
Explanation and overview here
|
|
||||||
|
|||||||
Reference in New Issue
Block a user