diff --git a/backend/controllers/calls.js b/backend/controllers/calls.js index e20c61e..573a829 100644 --- a/backend/controllers/calls.js +++ b/backend/controllers/calls.js @@ -1,6 +1,6 @@ const { ObjectId } = require('mongodb'); -var { callModel: Call } = require("../models/call"); +const Call = require("../models/call"); const Group = require("../models/group"); const opentelemetry = require('@opentelemetry/api'); var defaultNumResults = 50; diff --git a/backend/controllers/events.js b/backend/controllers/events.js index 09614d0..b8a3203 100644 --- a/backend/controllers/events.js +++ b/backend/controllers/events.js @@ -6,7 +6,8 @@ var Podcast = require("../models/podcast"); var System = require("../models/system"); var Talkgroup = require("../models/talkgroup"); const AdmZip = require("adm-zip"); -var { callModel: Call, frozenCallModel: FrozenCall } = require("../models/call"); +const Call = require("../models/call"); +const FrozenCall = require("../models/frozenCall"); var frontend_server = process.env['REACT_APP_FRONTEND_SERVER'] != null ? process.env['REACT_APP_FRONTEND_SERVER'] : 'https://openmhz.com'; const os = require('os'); diff --git a/backend/controllers/uploads.js b/backend/controllers/uploads.js index 41e9ab0..8bd3d78 100644 --- a/backend/controllers/uploads.js +++ b/backend/controllers/uploads.js @@ -2,11 +2,11 @@ const path = require('path'); const fs = require('fs'); const opentelemetry = require('@opentelemetry/api'); const sysStats = require("../sys_stats"); -const System = require("../models/system"); -const Talkgroup = require("../models/talkgroup"); -const { callModel: Call } = require("../models/call"); +const systemSchema = require("../models/systemSchema"); +const talkgroupSchema = require("../models/talkgroupSchema"); +const callSchema = require("../models/callSchema"); const { trace, context } = opentelemetry; - +const mongoose = require("mongoose"); const { S3Client, PutObjectCommand } = require("@aws-sdk/client-s3"); const { fromIni } = require("@aws-sdk/credential-providers"); const { NodeHttpHandler } = require('@smithy/node-http-handler'); @@ -21,6 +21,31 @@ const s3_region = process.env['S3_REGION'] ?? 'us-west-1'; const s3_bucket = process.env['S3_BUCKET'] ?? 'openmhz-west'; const s3_profile = process.env['S3_PROFILE'] ?? 'wasabi-account'; const s3_public_url = process.env['S3_PUBLIC_URL'] ?? `${s3_endpoint}/${s3_bucket}`; +const host = process.env['MONGO_NODE_DRIVER_HOST'] != null ? process.env['MONGO_NODE_DRIVER_HOST'] : 'mongo'; +const port = process.env['MONGO_NODE_DRIVER_PORT'] != null ? process.env['MONGO_NODE_DRIVER_PORT'] : 27017; +const mongoUrl = 'mongodb://' + host + ':' + port + '/scanner'; + + +// `asPromise()` returns a promise that resolves to the connection +// once the connection succeeds, or rejects if connection failed. +const mongo_conn_slow = mongoose.createConnection(mongoUrl, { maxPoolSize: 150 }); +const mongo_conn_fast = mongoose.createConnection(mongoUrl, { maxPoolSize: 150 }); +mongo_conn_slow.on('error', console.error); +mongo_conn_fast.on('error', console.error); +mongo_conn_slow.on('disconnected', () => { + console.log('Mongo Slow Disconnected'); + mongo_conn_slow.openUri(mongoUrl, { maxPoolSize: 150 }).catch(console.error); +}); +mongo_conn_fast.on('disconnected', () => { + console.log('Mongo Fast Disconnected'); + mongo_conn_fast.openUri(mongoUrl, { maxPoolSize: 150 }).catch(console.error); +}); + +mongo_conn_slow.model('Call', callSchema); +mongo_conn_fast.model('System', systemSchema); +mongo_conn_fast.model('Talkgroup', talkgroupSchema); + + const client = new S3Client({ requestHandler: new NodeHttpHandler({ @@ -87,7 +112,7 @@ exports.upload = async function (req, res, next) { await tracer.startActiveSpan('validate_system', { parent: trace.getActiveSpan(context.active()) }, async (validateSpan) => { try { - item = await System.findOne({ shortName }, ["key", "ignoreUnknownTalkgroup"]); + item = await mongo_conn_fast.model("System").findOne({ shortName }, ["key", "ignoreUnknownTalkgroup"]); validateSpan.setAttribute('system.exists', !!item); } catch (err) { validateSpan.recordException(err); @@ -117,7 +142,7 @@ exports.upload = async function (req, res, next) { } validateSystemTime = Date.now(); if (item.ignoreUnknownTalkgroup) { - const talkgroupExists = await Talkgroup.exists({ + const talkgroupExists = await mongo_conn_fast.model("Talkgroup").exists({ shortName, num: talkgroupNum, }); @@ -139,7 +164,7 @@ exports.upload = async function (req, res, next) { const object_key = `media/${shortName}/${talkgroupNum}/${shortName}-${talkgroupNum}-${startTime}${path.extname(req.file.originalname)}`; const url = `${s3_public_url}/${object_key}`; - const call = new Call({ + const call = new (mongo_conn_slow.model("Call"))({ shortName, talkgroupNum, objectKey: object_key, diff --git a/backend/models/call.js b/backend/models/call.js index 06d6b55..ff464e0 100644 --- a/backend/models/call.js +++ b/backend/models/call.js @@ -1,45 +1,13 @@ var mongoose = require('mongoose'); +const callSchema = require('./callSchema'); /* { "_id" : ObjectId("5bbc148de26adf00068a0076"), "shortName" : "dcfd", "talkgroupNum" : 11619, "objectKey" : "dcfd-11619-1539052672.m4a", "endpoint" : "nyc3.digitaloceanspaces.com", "bucket" : "openmhz", "time" : ISODate("2018-10-09T02:37:52Z"), "name" : "11619-1539052672.m4a", "freq" : 856987500, "url" : "https://media.openmhz.com/dcfd/2018/10/9/11619-1539052672.m4a", "emergency" : 0, "path" : "/dcfd/2018/10/9/", "srcList" : [ { "pos" : 0, "src" : 1116743 }, { "pos" : 1.8, "src" : 1102467 }, { "pos" : 3.6, "src" : 1116743 } ], "freqList" : [ { "pos" : 0, "freq" : 856987500, "len" : 51744, "errors" : 170, "spikes" : 29 } ], "len" : 6.142 } */ -var srcSchema = mongoose.Schema({ pos: Number, src: String }); -var freqSchema = mongoose.Schema({ pos: Number, freq: Number, len: Number, error: Number, spikes: Number}); -var callSchema = mongoose.Schema({ - talkgroupNum: Number, - shortName: String, - objectKey: String, - endpoint: String, - bucket: String, - time: Date, - name: String, - freq: Number, - errorCount: Number, - spikeCount: Number, - url: String, - emergency: Boolean, - path: String, - len: Number, - star: { - type: Number, - default: 0 - }, - srcList: [srcSchema] -}); -var frozenCallSchema = mongoose.Schema(); -frozenCallSchema.add(callSchema).add({ - systemName: String, - systemDescription: String, - talkgroupDescription: String, - talkgroupAlpha: String, - -}); -exports.frozenCallSchema = frozenCallSchema; -exports.frozenCallModel = mongoose.model('FrozenCall', frozenCallSchema); -exports.callSchema = callSchema; -exports.callModel = mongoose.model('Call', callSchema); + +module.exports = mongoose.model('Call', callSchema); diff --git a/backend/models/callSchema.js b/backend/models/callSchema.js new file mode 100644 index 0000000..6dbcebf --- /dev/null +++ b/backend/models/callSchema.js @@ -0,0 +1,27 @@ +var mongoose = require('mongoose'); + +var srcSchema = mongoose.Schema({ pos: Number, src: String }); + +const callSchema = mongoose.Schema({ + talkgroupNum: Number, + shortName: String, + objectKey: String, + endpoint: String, + bucket: String, + time: Date, + name: String, + freq: Number, + errorCount: Number, + spikeCount: Number, + url: String, + emergency: Boolean, + path: String, + len: Number, + star: { + type: Number, + default: 0 + }, + srcList: [srcSchema] +}); + +module.exports = callSchema; \ No newline at end of file diff --git a/backend/models/event.js b/backend/models/event.js index da0169b..ed9bbc4 100644 --- a/backend/models/event.js +++ b/backend/models/event.js @@ -1,5 +1,5 @@ var mongoose = require('mongoose'); -var {frozenCallSchema} = require('./call'); +const frozenCallSchema = require('./frozenCallSchema'); var eventSchema = mongoose.Schema({ diff --git a/backend/models/frozenCall.js b/backend/models/frozenCall.js new file mode 100644 index 0000000..06139d3 --- /dev/null +++ b/backend/models/frozenCall.js @@ -0,0 +1,4 @@ +var mongoose = require('mongoose'); +const frozenCallSchema = require('./frozenCallSchema'); + +module.exports = mongoose.model('FrozenCall', frozenCallSchema); \ No newline at end of file diff --git a/backend/models/frozenCallSchema.js b/backend/models/frozenCallSchema.js new file mode 100644 index 0000000..801c262 --- /dev/null +++ b/backend/models/frozenCallSchema.js @@ -0,0 +1,14 @@ +var mongoose = require('mongoose'); +const callSchema = require('./callSchema'); + +var frozenCallSchema = mongoose.Schema(); + +frozenCallSchema.add(callSchema).add({ + systemName: String, + systemDescription: String, + talkgroupDescription: String, + talkgroupAlpha: String, + +}); + +module.exports = frozenCallSchema; \ No newline at end of file diff --git a/backend/models/system.js b/backend/models/system.js index c761751..fa19fb6 100644 --- a/backend/models/system.js +++ b/backend/models/system.js @@ -1,37 +1,6 @@ var mongoose = require('mongoose'); -var bcrypt = require('bcrypt-nodejs'); -var User = require('./user'); +const systemSchema = require('./systemSchema'); -var systemSchema = mongoose.Schema({ - name: String, - shortName: String, - systemType: String, - county: String, - country: String, - city: String, - state: String, - description: String, - status: String, - showScreenName: Boolean, - allowContact: { - type: Boolean, - default: true - }, - callAvg: Number, - callCount: Number, - ignoreUnknownTalkgroup : Boolean, - active: {type: Boolean, default: false}, - lastActive: Date, - userId: {type: mongoose.Schema.Types.ObjectId, ref: 'User'}, - key: String -}); - -systemSchema.methods.generateHash = function(password) { - return bcrypt.hashSync(password, bcrypt.genSaltSync(8), null); -}; - -systemSchema.methods.validPassword = function(password) { - return bcrypt.compareSync(password, this.local.password); -}; +exports.systemSchema = systemSchema; module.exports = mongoose.model('System', systemSchema); diff --git a/backend/models/systemSchema.js b/backend/models/systemSchema.js new file mode 100644 index 0000000..0f1557b --- /dev/null +++ b/backend/models/systemSchema.js @@ -0,0 +1,37 @@ +var mongoose = require('mongoose'); +var bcrypt = require('bcrypt-nodejs'); +var User = require('./user'); + +const systemSchema = mongoose.Schema({ + name: String, + shortName: String, + systemType: String, + county: String, + country: String, + city: String, + state: String, + description: String, + status: String, + showScreenName: Boolean, + allowContact: { + type: Boolean, + default: true + }, + callAvg: Number, + callCount: Number, + ignoreUnknownTalkgroup : Boolean, + active: {type: Boolean, default: false}, + lastActive: Date, + userId: {type: mongoose.Schema.Types.ObjectId, ref: 'User'}, + key: String + }); + + systemSchema.methods.generateHash = function(password) { + return bcrypt.hashSync(password, bcrypt.genSaltSync(8), null); + }; + + systemSchema.methods.validPassword = function(password) { + return bcrypt.compareSync(password, this.local.password); + }; + + module.exports = systemSchema; \ No newline at end of file diff --git a/backend/models/talkgroup.js b/backend/models/talkgroup.js index 0e9a812..44211d2 100644 --- a/backend/models/talkgroup.js +++ b/backend/models/talkgroup.js @@ -1,16 +1,4 @@ var mongoose = require('mongoose'); - -var talkgroupSchema = mongoose.Schema({ - shortName: String, - userId: String, - num: Number, - mode: String, - alpha: String, - description: String, - tag: String, - group: String, - priority: Number -}); - +const talkgroupSchema = require('./talkgroupSchema'); module.exports = mongoose.model('Talkgroup', talkgroupSchema); diff --git a/backend/models/talkgroupSchema.js b/backend/models/talkgroupSchema.js new file mode 100644 index 0000000..ce0fa39 --- /dev/null +++ b/backend/models/talkgroupSchema.js @@ -0,0 +1,15 @@ +var mongoose = require('mongoose'); + +const talkgroupSchema = mongoose.Schema({ + shortName: String, + userId: String, + num: Number, + mode: String, + alpha: String, + description: String, + tag: String, + group: String, + priority: Number +}); + +module.exports = talkgroupSchema; \ No newline at end of file diff --git a/frontend/package-lock.json b/frontend/package-lock.json index e0c6a8e..25583d8 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -24,6 +24,7 @@ "@visx/scale": "^3.0.0", "@visx/shape": "^3.0.0", "@visx/tooltip": "^3.1.2", + "@wavesurfer/react": "^1.0.7", "axios": "1.2.1", "d3-array": "^3.2.3", "d3-time-format": "^4.1.0", @@ -5783,6 +5784,15 @@ "react-dom": "^16.8.0-0 || ^17.0.0-0 || ^18.0.0-0" } }, + "node_modules/@wavesurfer/react": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/@wavesurfer/react/-/react-1.0.7.tgz", + "integrity": "sha512-Bw2rd2zqVL1ERy8N9mciacbQW3N5Gn9oRjhq3kjqgtLeE8a1WSTU+4kU0E37541jf432eNZ8lNzuodFFSiNccw==", + "peerDependencies": { + "react": "^18.2.0", + "wavesurfer.js": ">=7.7.14" + } + }, "node_modules/@webassemblyjs/ast": { "version": "1.11.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.1.tgz", @@ -17748,6 +17758,12 @@ "node": ">=10.13.0" } }, + "node_modules/wavesurfer.js": { + "version": "7.8.6", + "resolved": "https://registry.npmjs.org/wavesurfer.js/-/wavesurfer.js-7.8.6.tgz", + "integrity": "sha512-EDexkMwkkQBTWruhfWQRkTtvRggtKFTPuJX/oZ5wbIZEfyww9EBeLr2mtkxzA1S8TlWPx6adY5WyjOlNYNyHSg==", + "peer": true + }, "node_modules/wbuf": { "version": "1.7.3", "resolved": "https://registry.npmjs.org/wbuf/-/wbuf-1.7.3.tgz", diff --git a/frontend/package.json b/frontend/package.json index b435a5d..0daeee0 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -19,6 +19,7 @@ "@visx/scale": "^3.0.0", "@visx/shape": "^3.0.0", "@visx/tooltip": "^3.1.2", + "@wavesurfer/react": "^1.0.7", "axios": "1.2.1", "d3-array": "^3.2.3", "d3-time-format": "^4.1.0", diff --git a/frontend/public/silence.m4a b/frontend/public/silence.m4a new file mode 100644 index 0000000..c5036b2 Binary files /dev/null and b/frontend/public/silence.m4a differ diff --git a/frontend/src/Call/CallPlayer.js b/frontend/src/Call/CallPlayer.js index 2da2997..cb71813 100644 --- a/frontend/src/Call/CallPlayer.js +++ b/frontend/src/Call/CallPlayer.js @@ -6,7 +6,7 @@ import CallInfo from "./components/CallInfo"; import ListCalls from "./components/ListCalls"; import { useSelector, useDispatch } from 'react-redux' import { useGetTalkgroupsQuery } from '../features/api/apiSlice' -import { playedCall } from "../features/calls/callsSlice"; +import { playedCall } from "../features/calls/callsSlice"; import { useInView } from 'react-intersection-observer'; import { Container, @@ -18,7 +18,11 @@ import { Message, MessageHeader, Button, - ButtonContent + ButtonContent, + Checkbox, + GridColumn, Grid, + GridRow, + Form } from "semantic-ui-react"; import "./CallPlayer.css"; import queryString from '../query-string'; @@ -96,13 +100,13 @@ function CallPlayer(props) { if (currentIndex > 0) { const nextCallId = callsData.ids[currentIndex - 1]; const nextCall = callsData.entities[nextCallId]; - console.log("Autoplaying next call, current Index is: " + currentIndex + " isPlaying is: " + isPlaying ) + console.log("Autoplaying next call, current Index is: " + currentIndex + " isPlaying is: " + isPlaying) setCurrentCall(nextCall); dispatch(playedCall(nextCall._id)); setIsPlaying(true); } else if (backgroundAutoplay) { // there are no more calls to play, but we want to keep playing something so the webpage is not put to sleep, so we play silence! - setSilenceCount(silenceCount+1); + setSilenceCount(silenceCount + 1); } else { // there are no more calls to play and backgroundAutoplay is disabled setIsPlaying(false); @@ -132,20 +136,20 @@ function CallPlayer(props) { }, [loadOlderInView]); // This handles when a Call is specified in the URI -useEffect(() => { - if (props.initialCallId && callsData) { - const call = callsData.entities[props.initialCallId]; - if (call && !selectedInitialCall) { - const time = new Date(call.time); + useEffect(() => { + if (props.initialCallId && callsData) { + const call = callsData.entities[props.initialCallId]; + if (call && !selectedInitialCall) { + const time = new Date(call.time); - console.log("Playing Initial Call: " + call._id + " Start Time: " + time.toLocaleTimeString() ); - setIsPlaying(true); - setCurrentCall(call); - setSelectedInitialCall(true); // signals that it has selected the initial call - you only want to do this once - dispatch(playedCall(call._id)); + console.log("Playing Initial Call: " + call._id + " Start Time: " + time.toLocaleTimeString()); + setIsPlaying(true); + setCurrentCall(call); + setSelectedInitialCall(true); // signals that it has selected the initial call - you only want to do this once + dispatch(playedCall(call._id)); + } } - } -}, [props.initialCallId, callsData]) + }, [props.initialCallId, callsData]) // Triggered when a new call is selected in the parent component. // This happens when a new call comes over the socket @@ -153,13 +157,13 @@ useEffect(() => { // we should only set the selectCallId to be the current call when AutoPlay is selected // and when there isn't another call already playing useEffect(() => { - console.log("New selected call: " + selectCallId + " IsPlaying: " + isPlaying + " autoPlay: " + autoPlay ); + console.log("New selected call: " + selectCallId + " IsPlaying: " + isPlaying + " autoPlay: " + autoPlay); if (selectCallId && callsData && !isPlaying && autoPlay) { const call = callsData.entities[selectCallId]; if (call) { const time = new Date(call.time); - console.log("Playing Selected Call: " + call._id + " Start Time: " + time.toLocaleTimeString() ); + console.log("Playing Selected Call: " + call._id + " Start Time: " + time.toLocaleTimeString()); setIsPlaying(true); setCurrentCall(call); dispatch(playedCall(call._id)); @@ -169,7 +173,7 @@ useEffect(() => { useEffect(() => { if (system && system.status && system.status.length > 0) { - setStatusVisible(true); + setStatusVisible(true); } }, [system]) @@ -177,13 +181,17 @@ useEffect(() => { setStatusVisible(false); } + let autoplayClassName = "button-item"; + if (autoPlay) { + autoplayClassName += " active"; + } return (