diff --git a/src/injectscripts/all.js b/src/injectscripts/all.js
index 245f2ec7..4ee3d583 100644
--- a/src/injectscripts/all.js
+++ b/src/injectscripts/all.js
@@ -133,6 +133,9 @@ whenWeAreReady(function () {
Delete All Collections \
\
\
+ User Guides \
+ \
+ \
About Lighthouse \
\
\
@@ -178,18 +181,6 @@ whenWeAreReady(function () {
unitName +
' Today)\
\
- \
- \
- \
- Register Tab For Remote Control \
- \
\
\
\
- About Lighthouse \
+ User Guides \
+ \
+ \
+ About Lighthouse \
\
\
\
diff --git a/src/pages/lib/shared_token_code.js b/src/pages/lib/shared_token_code.js
index 420adaf9..edc1d33f 100644
--- a/src/pages/lib/shared_token_code.js
+++ b/src/pages/lib/shared_token_code.js
@@ -1,128 +1,89 @@
var $ = require('jquery');
var moment = require('moment');
-var periodicCheck = null
+var periodicCheck = null;
// wait for token to have loaded
-export function fetchBeaconToken(apiHost, source, cb) { //when external vars have loaded
- var waiting = setInterval(function () { //run every 1sec until we have loaded the page (dont hate me Sam)
+export function fetchBeaconToken(apiHost, source, cb) {
+ const waiting = setInterval(function () {
chrome.storage.local.get('beaconAPIToken-' + apiHost, function (data) {
- var tokenJSON = JSON.parse(data['beaconAPIToken-' + apiHost])
- if (typeof tokenJSON.token !== "undefined" && typeof tokenJSON.expdate !== "undefined" && tokenJSON.token != '' && tokenJSON.expdate != '') {
- var token = tokenJSON.token
- var tokenexp = tokenJSON.expdate
- clearInterval(waiting); //stop timer
+ const tokenJSON = JSON.parse(data['beaconAPIToken-' + apiHost]);
+ if (tokenJSON?.token && tokenJSON?.expdate) {
+ const { token, expdate: tokenexp } = tokenJSON;
+ clearInterval(waiting); // stop timer
- if (periodicCheck == null) {
- periodicCheck = setInterval(function () {
- validateBeaconToken(apiHost, source)
- }, 3e5);
- }
-
- cb({
- token,
- tokenexp
- }); //call back
+ startTokenValidation(apiHost, source, cb); // Start periodic validation
+ cb({ token, tokenexp }); // callback with token
}
- })
+ });
}, 200);
}
-// wait for token to have loaded
-export function fetchBeaconTokenAndKeepReturningValidTokens(apiHost, source, cb) { //when external vars have loaded
- var waiting = setInterval(function () { //run every 1sec until we have loaded the page (dont hate me Sam)
+// wait for token to have loaded and keep returning valid tokens
+export function fetchBeaconTokenAndKeepReturningValidTokens(apiHost, source, cb) {
+ const waiting = setInterval(function () {
chrome.storage.local.get('beaconAPIToken-' + apiHost, function (data) {
- var tokenJSON = JSON.parse(data['beaconAPIToken-' + apiHost])
- if (typeof tokenJSON.token !== "undefined" && typeof tokenJSON.expdate !== "undefined" && tokenJSON.token != '' && tokenJSON.expdate != '') {
- var token = tokenJSON.token
- var tokenexp = tokenJSON.expdate
- clearInterval(waiting); //stop timer
+ const tokenJSON = JSON.parse(data['beaconAPIToken-' + apiHost]);
+ if (tokenJSON?.token && tokenJSON?.expdate) {
+ const { token, expdate: tokenexp } = tokenJSON;
+ clearInterval(waiting); // stop timer
- if (periodicCheck == null) {
- periodicCheck = setInterval(function () {
- validateBeaconTokenKeepReturning(apiHost, source, cb)
- }, 1 * 60 * 1000); // 1 minute
- }
-
- cb({
- token,
- tokenexp
- }); //call back
+ startTokenValidation(apiHost, source, cb); // Start periodic validation
+ cb({ token, tokenexp }); // callback with token
}
- })
+ });
}, 200);
}
-function validateBeaconTokenKeepReturning(apiHost, source, cb) {
- fetchBeaconToken(apiHost, source, function ({
- token,
- tokenexp
- }) {
- if (moment().isAfter(moment(tokenexp).subtract(5, "minutes"))) {
- console.log("token expiry triggered. time to renew.")
- $.ajax({
- type: 'GET',
- url: source + "/Authorization/RefreshToken",
- beforeSend: function (n) {
- n.setRequestHeader("Authorization", "Bearer " + token)
- },
- cache: false,
- dataType: 'json',
- complete: function (response, _textStatus) {
- var token = response.responseJSON.access_token
- var tokenexp = response.responseJSON.expires_at
- chrome.storage.local.set({
- ['beaconAPIToken-' + apiHost]: JSON.stringify({
- token: token,
- expdate: tokenexp
- })
- }, function () {
- console.log('local data set - beaconAPIToken')
- cb({
- token,
- tokenexp
- }); //call back again
- })
- console.log("successful token renew.")
+// Start a single periodic token validation timer
+function startTokenValidation(apiHost, source, cb) {
+ if (periodicCheck != null) return; // Avoid multiple timers
+
+ periodicCheck = setInterval(function () {
+ chrome.storage.local.get('beaconAPIToken-' + apiHost, function (data) {
+ const tokenJSON = JSON.parse(data['beaconAPIToken-' + apiHost]);
+ if (tokenJSON?.token && tokenJSON?.expdate) {
+ const { token, expdate: tokenexp } = tokenJSON;
+
+ if (moment().isAfter(moment(tokenexp).subtract(5, "minutes"))) {
+ renewToken(apiHost, source, token, cb);
+ } else {
+ console.log('API token still valid');
}
- })
- } else {
- console.log('api token still valid')
- }
- })
+ }
+ });
+ }, 1 * 60 * 1000); // Check every 1 minute
}
-
-export function validateBeaconToken(apiHost, source) {
- fetchBeaconToken(apiHost, source, function ({
- token,
- tokenexp
- }) {
- if (moment().isAfter(moment(tokenexp).subtract(5, "minutes"))) {
- console.log("token expiry triggered. time to renew.")
- $.ajax({
- type: 'GET',
- url: source + "/Authorization/RefreshToken",
- beforeSend: function (n) {
- n.setRequestHeader("Authorization", "Bearer " + token)
- },
- cache: false,
- dataType: 'json',
- complete: function (response, _textStatus) {
- var token = response.responseJSON.access_token
- var tokenexp = response.responseJSON.expires_at
- chrome.storage.local.set({
- ['beaconAPIToken-' + apiHost]: JSON.stringify({
- token: token,
- expdate: tokenexp
- })
- }, function () {
- console.log('local data set - beaconAPIToken')
+// Renew the token
+function renewToken(apiHost, source, token, cb) {
+ console.log("Token expiry triggered. Attempting to renew.");
+ $.ajax({
+ type: 'GET',
+ url: source + "/Authorization/RefreshToken",
+ beforeSend: function (n) {
+ n.setRequestHeader("Authorization", "Bearer " + token);
+ },
+ cache: false,
+ dataType: 'json',
+ complete: function (response, _textStatus) {
+ if (response.status === 200 && response.responseJSON) {
+ const newToken = response.responseJSON.access_token;
+ const newTokenExp = response.responseJSON.expires_at;
+ chrome.storage.local.set({
+ ['beaconAPIToken-' + apiHost]: JSON.stringify({
+ token: newToken,
+ expdate: newTokenExp
})
- console.log("successful token renew.")
- }
- })
- } else {
- console.log('api token still valid')
+ }, function () {
+ console.log('Local data set - beaconAPIToken');
+ cb({ token: newToken, tokenexp: newTokenExp }); // callback with new token
+ });
+ console.log("Successful token renewal.");
+ } else {
+ console.error("Token renewal failed. Stopping further attempts.");
+ clearInterval(periodicCheck); // Stop further attempts
+ periodicCheck = null;
+ }
}
- })
+ });
}
diff --git a/src/pages/tasking/components/asset_popup.js b/src/pages/tasking/components/asset_popup.js
index c8746abc..698a7bcf 100644
--- a/src/pages/tasking/components/asset_popup.js
+++ b/src/pages/tasking/components/asset_popup.js
@@ -99,17 +99,17 @@ export function buildAssetPopupKO() {
-
-
+
+
+
+
+
+
+
-
@@ -129,7 +129,7 @@ export function buildAssetPopupKO() {
-
+
`;
diff --git a/src/pages/tasking/components/job_popup.js b/src/pages/tasking/components/job_popup.js
index 8d3f257e..716a2833 100644
--- a/src/pages/tasking/components/job_popup.js
+++ b/src/pages/tasking/components/job_popup.js
@@ -139,7 +139,12 @@ export function buildJobPopupKO() {
+
+
+
+
+
{
+ self.trackableAssetsModalVM.isOpen(false);
+ });
+ }
+ }
+
// Job registry/upsert
// might be called from tasking OR job fetch so values might be missing
@@ -1520,7 +1549,11 @@ function VM() {
self.fetchAllTeamData = async function () {
const hqsFilter = this.config.teamFilters().map(f => ({ Id: f.id }));
- const statusFilterToView = myViewModel.config.teamStatusFilter().map(status => Enum.TeamStatusType[status]?.Id).filter(id => id !== undefined);
+ const statusFilterToView = myViewModel.config.teamStatusFilter().map(desc => {
+ // Find the Enum.TeamStatusType entry whose Description matches desc
+ const entry = Object.values(Enum.TeamStatusType).find(e => e.Description === desc);
+ return entry ? entry.Id : undefined;
+ }).filter(id => id !== undefined);
var end = new Date();
var start = new Date();
start.setDate(end.getDate() - myViewModel.config.fetchPeriod());
@@ -2184,14 +2217,14 @@ document.addEventListener('DOMContentLoaded', function () {
});
});
+})
+// wait for full CSS + DOM
+window.addEventListener('load', function () {
+ document.body.style.opacity = '1';
+});
- // wait for full CSS + DOM
- window.addEventListener('load', function () {
- document.body.style.opacity = '1';
- });
-})
function getSearchParameters() {
var prmstr = window.location.search.substr(1);
@@ -2206,4 +2239,4 @@ function transformToAssocArray(prmstr) {
params[tmparr[0]] = decodeURIComponent(tmparr[1]);
}
return params;
-}
\ No newline at end of file
+}
diff --git a/src/pages/tasking/models/Asset.js b/src/pages/tasking/models/Asset.js
index f7fc4c7c..d74a11cf 100644
--- a/src/pages/tasking/models/Asset.js
+++ b/src/pages/tasking/models/Asset.js
@@ -13,10 +13,11 @@ export function Asset(data = {}) {
self.entity = ko.observable(data.properties.entity ?? "");
self.resourceType = ko.observable(data.properties.resourceType ?? "");
self.lastSeen = ko.observable(data.lastSeen ?? "");
- self.licensePlate = ko.observable(data.properties.licensePlate ?? "");
+ self.licensePlate = ko.observable(data.properties.licensePlate ?? "-");
self.direction = ko.observable(data.properties.direction ?? null);
self.talkgroup = ko.observable(data.properties.talkgroup ?? "");
self.talkgroupLastUpdated = ko.observable(data.properties.talkgroupLastUpdated ?? "");
+ self.radioId = ko.observable(data.properties.radioId ?? "");
self.marker = null;
self.matchingTeams = ko.observableArray();
@@ -24,6 +25,14 @@ export function Asset(data = {}) {
return self.matchingTeams().filter(t => t.isFilteredIn());
});
+ self.lastSeenJustAgoText = ko.pureComputed(() => {
+ const v = safeStr(self.lastSeen?.());
+ if (!v) return "";
+ const d = new Date(v);
+ if (isNaN(d)) return v;
+ return fmtRelative(d);
+ });
+
self.lastSeenText = ko.pureComputed(() => {
const v = safeStr(self.lastSeen?.());
if (!v) return "";
diff --git a/src/pages/tasking/viewmodels/AssetPopUp.js b/src/pages/tasking/viewmodels/AssetPopUp.js
index 26209aef..39a83e20 100644
--- a/src/pages/tasking/viewmodels/AssetPopUp.js
+++ b/src/pages/tasking/viewmodels/AssetPopUp.js
@@ -1,5 +1,6 @@
var L = require('leaflet');
var moment = require('moment');
+var ko = require('knockout');
export class AssetPopupViewModel {
constructor({ asset, map, api }) {
@@ -8,6 +9,8 @@ export class AssetPopupViewModel {
this.api = api;
}
+ routeLoading = ko.observable(false);
+
openBeaconEditTeam = (team) => {
team.openBeaconEditTeam();
@@ -30,6 +33,8 @@ export class AssetPopupViewModel {
this.api.clearCrowFliesLine();
}
+
+
drawRouteToJob = (tasking) => {
const from = tasking.getTeamLatLng();
const to = tasking.getJobLatLng();
@@ -37,7 +42,7 @@ export class AssetPopupViewModel {
console.warn('Cannot draw route: missing team or job coordinates.');
return;
}
-
+ this.routeLoading(true);
const routeControl = L.Routing.control({
waypoints: [from, to],
router: L.Routing.graphHopper('lighthouse', {
@@ -117,14 +122,15 @@ export class AssetPopupViewModel {
if (s && d && routeControl) routeControl.setWaypoints([s, d]);
}));
}
+ this.routeLoading(false);
});
}
- removeRouteToJob = () => {
- this.api.clearRoutes();
- }
+ removeRouteToJob = () => {
+ this.api.clearRoutes();
+ }
// Example hook – wire this to your app-level handler if needed.
assignTeam() {
diff --git a/src/pages/tasking/viewmodels/JobPopUp.js b/src/pages/tasking/viewmodels/JobPopUp.js
index dfcb90ab..f813bbab 100644
--- a/src/pages/tasking/viewmodels/JobPopUp.js
+++ b/src/pages/tasking/viewmodels/JobPopUp.js
@@ -1,5 +1,6 @@
var L = require('leaflet');
var moment = require('moment');
+var ko = require('knockout');
export class JobPopupViewModel {
@@ -8,6 +9,9 @@ export class JobPopupViewModel {
this.api = api;
}
+ routeLoading = ko.observable(false);
+
+
updatePopup = () => {
if (this.job.marker && this.job.marker.isPopupOpen()) {
const popup = this.job.marker.getPopup();
@@ -46,7 +50,7 @@ export class JobPopupViewModel {
console.warn('Cannot draw route: missing team or job coordinates.');
return;
}
-
+ this.routeLoading(true);
const routeControl = L.Routing.control({
waypoints: [from, to],
router: L.Routing.graphHopper('lighthouse', {
@@ -126,6 +130,7 @@ export class JobPopupViewModel {
if (s && d && routeControl) routeControl.setWaypoints([s, d]);
}));
}
+ this.routeLoading(false);
});
}
diff --git a/src/pages/tasking/viewmodels/TrackableAssetsModalVM.js b/src/pages/tasking/viewmodels/TrackableAssetsModalVM.js
new file mode 100644
index 00000000..caccc00f
--- /dev/null
+++ b/src/pages/tasking/viewmodels/TrackableAssetsModalVM.js
@@ -0,0 +1,32 @@
+/* eslint-disable @typescript-eslint/no-this-alias */
+import ko from "knockout";
+
+// ViewModel for the Trackable Assets modal
+export function TrackableAssetsModalVM(mainVM) {
+ const self = this;
+ self.searchQuery = ko.observable('');
+ self.talkgroups = ko.observableArray([]);
+ self.selectedTalkgroup = ko.observable();
+ self.isOpen = ko.observable(false);
+ // Compute unique talkgroups from all assets
+ ko.computed(() => {
+ const allAssets = mainVM.trackableAssets();
+ const groups = Array.from(new Set(allAssets.map(a => a.talkgroup && a.talkgroup())));
+ self.talkgroups(groups.filter(Boolean));
+ });
+
+ self.filteredAssets = ko.pureComputed(() => {
+ if (!self.isOpen) return [];
+ const query = self.searchQuery().toLowerCase();
+ const tg = self.selectedTalkgroup();
+ return mainVM.trackableAssets().filter(a => {
+ const name = a.name && a.name().toLowerCase();
+ const radioId = a.radioId && String(a.radioId()).toLowerCase();
+ const talkgroup = a.talkgroup && a.talkgroup();
+ const entity = a.entity && a.entity().toLowerCase();
+ const matchesQuery = !query || (name && name.includes(query)) || (radioId && radioId.includes(query)) || (entity && entity.includes(query));
+ const matchesTG = !tg || talkgroup === tg;
+ return matchesQuery && matchesTG;
+ });
+ });
+}
\ No newline at end of file
diff --git a/static/pages/tasking.html b/static/pages/tasking.html
index c44b2415..4587c19a 100644
--- a/static/pages/tasking.html
+++ b/static/pages/tasking.html
@@ -34,10 +34,10 @@
-
-
+
+
+
+
+
+
+ Trackable Asset Library
+
Task Count
- Team Leader
- Actions
+ Team Leader
+ Actions
@@ -199,9 +207,9 @@
-
@@ -559,7 +567,8 @@
-
+
-
-
+
-
@@ -1397,7 +1406,8 @@
- Override set time
+ Override set
+ time
for this status
@@ -1498,7 +1508,8 @@
-
@@ -1561,10 +1571,10 @@
-
-
-
-
+
+
+
+
@@ -2322,13 +2332,13 @@