Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
62 commits
Select commit Hold shift + click to select a range
60bf5f7
Tasking WIP into dev (#218)
OSPFNeighbour Nov 13, 2025
957e974
Tdykes.tasking.wip (#220)
OSPFNeighbour Nov 17, 2025
bca2ae1
Tdykes.tasking.wip (#221)
OSPFNeighbour Nov 17, 2025
85d65ec
Tdykes.tasking.wip (#223)
OSPFNeighbour Nov 18, 2025
12bb64d
Tdykes.tasking.wip (#224)
OSPFNeighbour Nov 24, 2025
d8f5728
Tdykes.tasking.wip (#225)
OSPFNeighbour Nov 24, 2025
b23c728
Tdykes.tasking.wip (#226)
OSPFNeighbour Nov 25, 2025
1636266
Tdykes.tasking.wip (#227)
OSPFNeighbour Nov 26, 2025
c143cdf
Tdykes.tasking.wip (#228)
OSPFNeighbour Nov 27, 2025
a429dea
Tdykes.tasking.wip (#229)
OSPFNeighbour Nov 27, 2025
96a605c
Tdykes.tasking.wip (#230)
OSPFNeighbour Nov 27, 2025
53308e5
Tdykes.tasking.wip (#231)
OSPFNeighbour Nov 28, 2025
5a8f1a9
Tdykes.tasking.wip (#232)
OSPFNeighbour Nov 28, 2025
ecf3417
Tdykes.tasking.wip (#233)
OSPFNeighbour Nov 28, 2025
5d23087
Tdykes.tasking.wip (#234)
OSPFNeighbour Nov 28, 2025
4e5098e
Tdykes.tasking.wip (#235)
OSPFNeighbour Nov 29, 2025
b0cf70f
Tdykes.tasking.wip (#236)
OSPFNeighbour Nov 29, 2025
64f0cfd
Tdykes.tasking.wip (#237)
OSPFNeighbour Nov 30, 2025
fc067ac
Tdykes.tasking.wip (#238)
OSPFNeighbour Dec 1, 2025
9a4835d
Tdykes.tasking.wip (#239)
OSPFNeighbour Dec 2, 2025
ddab065
Tdykes.tasking.wip (#240)
OSPFNeighbour Dec 2, 2025
957ff34
Tdykes.tasking.wip (#241)
OSPFNeighbour Dec 3, 2025
c5435d5
Tdykes.tasking.wip (#242)
OSPFNeighbour Dec 3, 2025
08f55a2
Tdykes.tasking.wip (#243)
OSPFNeighbour Dec 3, 2025
1f7b2b9
Tdykes.tasking.wip (#244)
OSPFNeighbour Dec 4, 2025
49b3662
Tdykes.tasking.wip (#245)
OSPFNeighbour Dec 5, 2025
c7d78ca
Tdykes.tasking.wip (#246)
OSPFNeighbour Dec 8, 2025
e837d11
Tdykes.tasking.wip (#247)
OSPFNeighbour Dec 8, 2025
f0cb1d3
Tdykes.tasking.wip (#248)
OSPFNeighbour Dec 8, 2025
b147443
Tdykes.tasking.wip (#249)
OSPFNeighbour Dec 8, 2025
d71ca72
Tdykes.tasking.wip (#250)
OSPFNeighbour Dec 9, 2025
576c777
Tdykes.tasking.wip (#251)
OSPFNeighbour Dec 9, 2025
2232abc
Tdykes.tasking.wip (#252)
OSPFNeighbour Dec 13, 2025
d45390b
Tdykes.tasking.wip (#253)
OSPFNeighbour Dec 15, 2025
68c7f76
Tdykes.tasking.wip (#254)
OSPFNeighbour Dec 16, 2025
6017a30
Tdykes.tasking.wip (#255)
OSPFNeighbour Dec 16, 2025
fd414af
Tdykes.tasking.wip (#256)
OSPFNeighbour Dec 17, 2025
b8cd5de
Tasking Status Dropdown Rework (#257)
Rowantrek Dec 17, 2025
fda144d
fix logic fuckup (#258)
OSPFNeighbour Dec 17, 2025
52783ee
Lad wip tdykes (#259)
OSPFNeighbour Dec 17, 2025
585895f
Fixed up HTML for the teams cards (#260)
OSPFNeighbour Dec 18, 2025
f29ca9e
Added environmental themes (#261)
OSPFNeighbour Dec 19, 2025
b7e1221
fix bad logic in Can() blocks for jobs (#262)
OSPFNeighbour Dec 19, 2025
71bbb0f
collapse all buttons and sector fix (#263)
OSPFNeighbour Dec 19, 2025
ae29e90
Lad wip tdykes (#264)
OSPFNeighbour Dec 22, 2025
432564b
Handle alerts when pinned better (#265)
OSPFNeighbour Dec 23, 2025
09dcd18
added hazard watch layers (#266)
OSPFNeighbour Dec 26, 2025
139ef87
holding space hides map pops (#267)
OSPFNeighbour Dec 26, 2025
ef09766
New OpsLog Entry UI Overhaul + Minor Fixes. (#268)
Rowantrek Dec 26, 2025
3593a6f
minor UI fixes (#269)
OSPFNeighbour Dec 30, 2025
6baaf38
pinned to starred. token refresh tweaks
OSPFNeighbour Jan 11, 2026
149fe92
rewritten token code
OSPFNeighbour Jan 11, 2026
bdefae4
ID spacing fix
OSPFNeighbour Jan 11, 2026
b78248f
bad link on asset popup
OSPFNeighbour Jan 14, 2026
9a0cc75
progress indicator on route pathing loading
OSPFNeighbour Jan 15, 2026
ceac467
user guide link
OSPFNeighbour Jan 15, 2026
d408b82
bug fix where primary key was used not description
OSPFNeighbour Jan 15, 2026
0c9327f
Trackable Asset Library
OSPFNeighbour Jan 16, 2026
c705cdd
header width fix on tables
OSPFNeighbour Jan 16, 2026
32cad5a
wording fix
OSPFNeighbour Jan 16, 2026
a950534
width clamping
OSPFNeighbour Jan 16, 2026
2896b70
Merge branch 'master' into master-dev
OSPFNeighbour Jan 16, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 7 additions & 13 deletions src/injectscripts/all.js
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,9 @@ whenWeAreReady(function () {
<a href="#" id="LHClearStorage">Delete All Collections</a>\
</li>\
<li>\
<a href="http://lighthouse.ses.nsw.gov.au/guides.html">User Guides</a>\
</li>\
<li>\
<a href="http://lighthouse.ses.nsw.gov.au">About Lighthouse</a>\
</li>\
</ul>\
Expand Down Expand Up @@ -178,18 +181,6 @@ whenWeAreReady(function () {
unitName +
' Today)</a>\
</li>\
<li role="presentation" class="divider"></li><li role="presentation" class="dropdown-header">Tasking\
</li>\
<li id="lhtaskingmenuitem">\
<a href="' +
lighthouseUrl +
'pages/tasking.html' +
vars +
'" target="_blank">Lighthouse Aided Dispatch (LAD!)</a>\
</li>\
<li id="lhtaskingremoteregister">\
<a href="">Register Tab For Remote Control</a>\
</li>\
<li role="presentation" class="divider"></li><li role="presentation" class="dropdown-header">Teams\
</li>\
<li id="lhteammenuitem">\
Expand All @@ -207,7 +198,10 @@ whenWeAreReady(function () {
<a href="#" id="LHClearStorage">Delete All Collections</a>\
</li>\
<li>\
<a href="https://github.com/NSWSESMembers/Lighthouse/blob/master/README.md">About Lighthouse</a>\
<a href="http://lighthouse.ses.nsw.gov.au/guides.html">User Guides</a>\
</li>\
<li>\
<a href="http://lighthouse.ses.nsw.gov.au">About Lighthouse</a>\
</li>\
</ul>\
</li>\
Expand Down
173 changes: 67 additions & 106 deletions src/pages/lib/shared_token_code.js
Original file line number Diff line number Diff line change
@@ -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;
}
}
})
});
}
18 changes: 9 additions & 9 deletions src/pages/tasking/components/asset_popup.js
Original file line number Diff line number Diff line change
Expand Up @@ -99,17 +99,17 @@ export function buildAssetPopupKO() {

<!-- Action buttons -->
<div class="btn-group btn-group-sm ms-1" role="group">
<button class="btn btn-outline-secondary"
title="Focus on job"
data-bind="click: $root.safeJobFocus,
disable: !hasJob(),
clickBubble:false">
<i class="fa fa-crosshairs"></i>
<button type="button" class="btn btn-small btn-outline-secondary" title="Route to Job" data-bind="click: $root.drawRouteToJob, disable: !tsk.hasJob(), clickBubble: false">
<!-- ko ifnot: $root.routeLoading -->
<i class="fa fa-solid fa-car"></i>
<!-- /ko -->
<!-- ko if: $root.routeLoading -->
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
<!-- /ko -->
</button>

<button class="btn btn-outline-secondary"
title="Open in Beacon"
data-bind="click: $root.safeJobOpen,
data-bind="click: job.openBeaconJobDetails,
disable: !hasJob(),
clickBubble:false">
<i class="fa fa-external-link-alt"></i>
Expand All @@ -129,7 +129,7 @@ export function buildAssetPopupKO() {
</div>

<!-- Divider between teams -->
<hr class="my-2" data-bind="visible: $index() < ($parent.matchingTeams().length - 1)"/>
<hr class="my-2" data-bind="visible: $index() < ($parent.matchingTeamsInView().length - 1)"/>
</div>
</div>
</div>`;
Expand Down
5 changes: 5 additions & 0 deletions src/pages/tasking/components/job_popup.js
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,12 @@ export function buildJobPopupKO() {
<button type="button" class="btn btn-small btn-outline-secondary"
title="Route to Asset"
data-bind="click: $root.drawRouteToAsset, disable: !team.trackableAndIsFiltered(), clickBubble: false">
<!-- ko if: !$root.routeLoading() -->
<i class="fa fa-solid fa-car"></i>
<!-- /ko -->
<!-- ko if: $root.routeLoading() -->
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
<!-- /ko -->
</button>
<button type="button" class="btn btn-small btn-outline-secondary"
title="Fit Bounds"
Expand Down
47 changes: 40 additions & 7 deletions src/pages/tasking/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import { CreateOpsLogModalVM } from "./viewmodels/OpsLogModalVM.js";
import { CreateRadioLogModalVM } from "./viewmodels/RadioLogModalVM.js";
import { SendSMSModalVM } from "./viewmodels/SMSTeamModalVM.js";
import { JobStatusConfirmModalVM } from "./viewmodels/JobStatusConfirmModalVM.js";
import { TrackableAssetsModalVM } from "./viewmodels/TrackableAssetsModalVM.js";

import { installAlerts } from './components/alerts.js';
import { LegendControl } from './components/legend.js';
Expand Down Expand Up @@ -194,6 +195,8 @@ function VM() {
self.SendSMSModalVM = new SendSMSModalVM(self);
self.selectedJob = ko.observable(null);

self.trackableAssetsModalVM = new TrackableAssetsModalVM(self);

self.jobStatusConfirmVM = new JobStatusConfirmModalVM(self);

self.attachJobStatusConfirmModal = function (job, newStatus) {
Expand Down Expand Up @@ -915,6 +918,32 @@ function VM() {
});
};

self.openTrackableAssetsModal = function (data, event) {
// If called from a dropdown menu, close the dropdown
if (event && event.target) {
// Find the closest .dropdown and its .dropdown-toggle button
let dropdown = event.target.closest('.dropdown');
if (dropdown) {
let toggleBtn = dropdown.querySelector('[data-bs-toggle="dropdown"]');
if (toggleBtn) {
// Use Bootstrap 5 API to close the dropdown
let dropdownInstance = bootstrap.Dropdown.getOrCreateInstance(toggleBtn);
dropdownInstance.hide();
}
}
}
const modalEl = document.getElementById('trackableAssetsModal');
if (modalEl) {
self.trackableAssetsModalVM.isOpen(true)
bootstrap.Modal.getOrCreateInstance(modalEl).show();
}
if (modalEl) {
modalEl.addEventListener('hidden.bs.modal', () => {
self.trackableAssetsModalVM.isOpen(false);
});
}
}


// Job registry/upsert
// might be called from tasking OR job fetch so values might be missing
Expand Down Expand Up @@ -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());
Expand Down Expand Up @@ -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);
Expand All @@ -2206,4 +2239,4 @@ function transformToAssocArray(prmstr) {
params[tmparr[0]] = decodeURIComponent(tmparr[1]);
}
return params;
}
}
Loading
Loading