From 39207ff4d585160ae0f843ff403db20aa2fa94a2 Mon Sep 17 00:00:00 2001 From: Tim Dykes Date: Wed, 13 May 2026 21:00:03 +1000 Subject: [PATCH 1/4] Treat non-alphanumeric tokens as substrings Treat tokens that are purely numeric or contain non-alphanumeric characters as plain substrings instead of using word-boundary regexes. This updates both the jobSearchSuggestions regex logic and the termMatchers to use includes() for such tokens, avoiding missed matches for values like "14-7570" or "J-00123" and adding clarifying comments. --- src/pages/tasking/main.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/pages/tasking/main.js b/src/pages/tasking/main.js index a56729a1..bd76d098 100644 --- a/src/pages/tasking/main.js +++ b/src/pages/tasking/main.js @@ -715,8 +715,8 @@ function VM() { if (!lastToken) { self.jobSearchSuggestions([]); return; } const escapeRx = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); - const rx = /^\d+$/.test(lastToken) - ? null // numeric — use includes + const rx = (/^\d+$/.test(lastToken) || /[^a-z0-9]/i.test(lastToken)) + ? null // numeric or contains non-alphanumeric (e.g. "14-7570") — use includes : new RegExp('\\b' + escapeRx(lastToken), 'i'); // Helper: escape HTML entities so label text is safe for innerHTML @@ -911,8 +911,9 @@ function VM() { // surprising (e.g. "123" inside "J-00123" should still match). const escapeRx = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); const termMatchers = terms.map(t => { - if (/^\d+$/.test(t)) { - // Numeric token — plain substring is more intuitive + if (/^\d+$/.test(t) || /[^a-z0-9]/i.test(t)) { + // Numeric token or token containing non-alphanumeric chars (e.g. "14-7570") + // — plain substring is more intuitive and avoids \b boundary failures return (blob) => blob.includes(t); } const rx = new RegExp('\\b' + escapeRx(t), 'i'); From 716f47898c073488800f42da87cdc2bb3b480347 Mon Sep 17 00:00:00 2001 From: Tim Dykes Date: Wed, 13 May 2026 21:00:16 +1000 Subject: [PATCH 2/4] Add option to count only active taskings Introduce a config flag to have the status badge show only active taskings. Job.statusNameAndCount now checks deps.config?.taskingCountActiveOnly?() and, when enabled, counts only taskings whose currentStatus is Tasked, Enroute, or Onsite; otherwise it counts all taskings. ConfigVM: added taskingCountActiveOnly observable (default false), included it in serialization/loading, and auto-saved on change. UI: added a settings switch in tasking.html to toggle the behavior with explanatory text. --- src/pages/tasking/models/Job.js | 11 +++++++++-- src/pages/tasking/viewmodels/Config.js | 9 +++++++++ static/pages/tasking.html | 13 +++++++++++++ 3 files changed, 31 insertions(+), 2 deletions(-) diff --git a/src/pages/tasking/models/Job.js b/src/pages/tasking/models/Job.js index 985b4fdc..6f2fc13c 100644 --- a/src/pages/tasking/models/Job.js +++ b/src/pages/tasking/models/Job.js @@ -198,8 +198,15 @@ export function Job(data = {}, deps = {}) { self.statusNameAndCount = ko.pureComputed(() => { const statusName = self.statusName(); if (statusName === "Active" || statusName === "Tasked") { - const taskingCount = self.taskings().length; - return `${statusName} (${taskingCount})`; + const activeOnly = deps.config?.taskingCountActiveOnly?.(); + const taskings = self.taskings(); + const count = activeOnly + ? taskings.filter(t => { + const s = t.currentStatus?.(); + return s === "Tasked" || s === "Enroute" || s === "Onsite"; + }).length + : taskings.length; + return `${statusName} (${count})`; } return statusName; }); diff --git a/src/pages/tasking/viewmodels/Config.js b/src/pages/tasking/viewmodels/Config.js index 5655f782..91fb4831 100644 --- a/src/pages/tasking/viewmodels/Config.js +++ b/src/pages/tasking/viewmodels/Config.js @@ -267,6 +267,7 @@ export function ConfigVM(root, deps) { self.clusterRadius = ko.observable(60); // maxClusterRadius in px (10–80) self.clusterRescueJobs = ko.observable(true); self.alertsCollapsibleRules = ko.observable(true); + self.taskingCountActiveOnly = ko.observable(false); // pinned rows self.pinnedTeamIds = ko.observableArray([]); @@ -399,6 +400,7 @@ export function ConfigVM(root, deps) { clusterRadius: Number(self.clusterRadius()) || 60, clusterRescueJobs: !!self.clusterRescueJobs(), alertsCollapsibleRules: !!self.alertsCollapsibleRules(), + taskingCountActiveOnly: !!self.taskingCountActiveOnly(), suggestionEnabled: !!self.suggestionEnabled(), rescueDistanceWeight: Number(self.rescueDistanceWeight()) || 0, rescueTaskingWeight: Number(self.rescueTaskingWeight()) || 0, @@ -713,6 +715,9 @@ export function ConfigVM(root, deps) { if (typeof cfg.alertsCollapsibleRules === 'boolean') { self.alertsCollapsibleRules(cfg.alertsCollapsibleRules); } + if (typeof cfg.taskingCountActiveOnly === 'boolean') { + self.taskingCountActiveOnly(cfg.taskingCountActiveOnly); + } // Instant Task Suggestion Engine weights if (typeof cfg.suggestionEnabled === 'boolean') { @@ -905,6 +910,10 @@ export function ConfigVM(root, deps) { self.save(); }) + self.taskingCountActiveOnly.subscribe(() => { + self.save(); + }) + // Auto-save suggestion engine settings self.suggestionEnabled.subscribe(() => { self.save(); }); self.rescueDistanceWeight.subscribe(() => { self.save(); }); diff --git a/static/pages/tasking.html b/static/pages/tasking.html index 32997755..106bb642 100644 --- a/static/pages/tasking.html +++ b/static/pages/tasking.html @@ -2088,6 +2088,19 @@

Alerts will start collapsed.
+
+ +
+ + +
+
Status badge counts only Tasked, Enroute & Onsite taskings (excludes Complete, CalledOff, Untasked).
+
From a94773e1d86fb5e05568928f86df361e265a3cd4 Mon Sep 17 00:00:00 2001 From: Tim Dykes Date: Mon, 18 May 2026 13:37:07 +1000 Subject: [PATCH 3/4] Add 'prominent' alert modifier with styles Introduce a new boolean rule property `prominent` to visually emphasize urgent alerts. JS: accept and persist `prominent` on rules, toggle alerts--prominent class during in-place updates, set collapsed panel width from 24px to 30px, and mark the 'new-jobs' rule as prominent by default. CSS: add prominent styling and animations in styles/pages/tasking.css (opaque backgrounds, stronger borders, pulse/bounce, enlarged icon and badge) and darkmode overrides in styles/pages/darkmode.css. Minor layout tweak: collapsed alerts button padding/size adjusted to 30px to match the updated collapsed width. --- src/pages/tasking/components/alerts.js | 25 +++++--- styles/pages/darkmode.css | 6 ++ styles/pages/tasking.css | 82 +++++++++++++++++++++++++- 3 files changed, 104 insertions(+), 9 deletions(-) diff --git a/src/pages/tasking/components/alerts.js b/src/pages/tasking/components/alerts.js index 43d615a0..e1911143 100644 --- a/src/pages/tasking/components/alerts.js +++ b/src/pages/tasking/components/alerts.js @@ -31,11 +31,14 @@ function createLeafletControl(L) { /** * Render a list of active rules into the container. - RAW html no KO here yet - * Each rule: { id, level, title, items:[{id,label}], count, onClick? } - */ -/** - * Render a list of active rules into the container. - RAW html no KO here yet - * Each rule: { id, level, title, items:[{id,label}], count, onClick? } + * Each rule: { id, level, title, items:[{id,label}], count, onClick?, prominent? } + * + * prominent (boolean, optional) — when true the alert box draws extra attention: + * • opaque (rather than translucent) background for the level colour + * • 2 px solid border instead of 1 px semi-transparent + * • repeating pulse-glow box-shadow animation + * • enlarged hazard icon with a continuous ping ring + * • count wrapped in a solid filled badge */ function renderRules(container, rules, opts = {}) { const allowCollapse = opts.allowCollapse !== false; @@ -72,6 +75,8 @@ function renderRules(container, rules, opts = {}) { // --- IN-PLACE UPDATE: only patch count + items, preserve all user state --- const countEl = div.querySelector('.alerts__count'); if (countEl) countEl.textContent = rule.count; + // keep prominent class in sync + div.classList.toggle('alerts--prominent', !!rule.prominent); const ul = div.querySelector('.alerts__list'); if (ul) { @@ -94,9 +99,12 @@ function renderRules(container, rules, opts = {}) { div.setAttribute('data-rule-id', rule.id); var width = '280px' div.className = `leaflet-control alerts alerts--${rule.level}`; + if (rule.prominent) { + div.classList.add('alerts--prominent'); + } if (state.collapsed) { div.classList.add('alerts--collapsed'); - width = "24px" + width = "30px" } if (!state.collapsed && state.open) { div.classList.add('alerts--open'); @@ -160,10 +168,10 @@ function renderRules(container, rules, opts = {}) { btn.setAttribute('aria-expanded', 'false'); div.querySelector('.alerts').animate( - [{ width: '280px' }, { width: '24px' }], + [{ width: '280px' }, { width: '30px' }], { duration: 300, easing: 'ease-in-out' } ).onfinish = () => { - div.querySelector('.alerts').style.width = '24px'; + div.querySelector('.alerts').style.width = '30px'; }; div.classList.add('alerts--collapsed'); }); @@ -298,6 +306,7 @@ function buildDefaultRules(vm) { id: 'new-jobs', level: 'warning', title: 'Unacknowledged incidents', + prominent: true, active: newJobs.length > 0, items: newJobs.slice(0, 10).map(asItem), count: newJobs.length, diff --git a/styles/pages/darkmode.css b/styles/pages/darkmode.css index 611000b3..80377c70 100644 --- a/styles/pages/darkmode.css +++ b/styles/pages/darkmode.css @@ -644,6 +644,12 @@ body.dark-mode .leaflet-control.alerts.alerts--info { border: 1px solid rgba(95, 157, 255, 0.88) !important; } +/* Prominent modifier — dark mode */ +body.dark-mode .alerts--warning.alerts--prominent { background: rgba(160,110,0,0.95) !important; border: 2px solid #ffc107 !important; } +body.dark-mode .alerts--danger.alerts--prominent { background: rgba(140,20,30,0.95) !important; border: 2px solid #dc3545 !important; } +body.dark-mode .alerts--caution.alerts--prominent { background: rgba(8,66,152,0.95) !important; border: 2px solid #4d9aff !important; } +body.dark-mode .alerts--info.alerts--prominent { background: rgba(22,58,109,0.95) !important; border: 2px solid #5f9dff !important; } + /* Custom map layers drawer */ body.dark-mode .layers-drawer .ld-toggle-btn { background: #2d2d2d !important; diff --git a/styles/pages/tasking.css b/styles/pages/tasking.css index 4697b725..cbad87a3 100644 --- a/styles/pages/tasking.css +++ b/styles/pages/tasking.css @@ -1531,6 +1531,81 @@ overflow: hidden; } } +/* ── Prominent modifier ──────────────────────────────────────────────────── * + * Add prominent: true to any rule to make the alert visually urgent. + * Combines: opaque background, heavier border, repeating pulse-glow, + * enlarged icon, and a highlighted count badge. + * -------------------------------------------------------------------------- */ +.alerts--prominent { + border-width: 2px; + border-style: solid; + animation: + alerts-prominent-pulse 2s ease-in-out infinite, + alerts-prominent-bounce 1.2s cubic-bezier(0.36, 0.07, 0.19, 0.97) infinite; +} +/* Stop bouncing once the panel is open — keep the pulse-glow */ +.alerts--prominent.alerts--open { + animation: alerts-prominent-pulse 2s ease-in-out infinite; +} +.alerts--prominent .alerts__icon { + width: 22px; + height: 22px; + flex: 0 0 22px; +} +.alerts--prominent .alerts__icon::after { + content: ''; + position: absolute; + inset: -3px; + border-radius: 50%; + border: 2px solid currentColor; + opacity: 0; + animation: alerts-ping-loop 2s ease-out infinite; +} +.alerts--prominent .alerts__count { + padding: 1px 7px; + border-radius: 999px; + font-size: 12px; + font-weight: 700; +} +/* Per-level badge colours — explicit so they never rely on currentColor */ +.alerts--warning.alerts--prominent .alerts__count { background: #7a4800; color: #fff; } +.alerts--danger.alerts--prominent .alerts__count { background: #6b0d17; color: #fff; } +.alerts--caution.alerts--prominent .alerts__count { background: #052c65; color: #fff; } +.alerts--info.alerts--prominent .alerts__count { background: #053d22; color: #fff; } +/* Override the count text colour so it reads white over the solid badge */ +.alerts--prominent .alerts__count { + filter: none; +} +/* Make the badge background use the level accent colour */ +.alerts--warning.alerts--prominent { background: rgba(255,193,7,0.92); border-color: #e6a800; } +.alerts--danger.alerts--prominent { background: rgba(220,53,69,0.92); border-color: #b02a37; } +.alerts--caution.alerts--prominent { background: rgba(13,110,253,0.88); border-color: #0a58ca; color: #fff; } +.alerts--info.alerts--prominent { background: rgba(25,135,84,0.88); border-color: #0f6848; color: #fff; } + +/* Force text legibility for levels that go dark-background */ +.alerts--caution.alerts--prominent .alerts__panel, +.alerts--info.alerts--prominent .alerts__panel { color: #fff; border-top-color: rgba(255,255,255,0.4); } + +@keyframes alerts-prominent-pulse { + 0%,100% { box-shadow: 0 0 6px 0 currentColor; } + 50% { box-shadow: 0 0 18px 4px currentColor; } +} +@keyframes alerts-prominent-bounce { + 0%,100% { transform: translateY(0); } + 10% { transform: translateY(-8px); } + 20% { transform: translateY(0); } + 30% { transform: translateY(-5px); } + 40% { transform: translateY(0); } + 50% { transform: translateY(-2px); } + 60%,99% { transform: translateY(0); } +} + +@keyframes alerts-ping-loop { + 0% { transform: scale(0.8); opacity: 0.8; } + 70% { transform: scale(1.8); opacity: 0.2; } + 100% { transform: scale(2.4); opacity: 0; } +} + /* Subtle ping from the hazard icon once */ .alerts__btn .alerts__icon { position: relative; @@ -2672,8 +2747,13 @@ overflow: hidden; } .leaflet-control.alerts.alerts--collapsed .alerts__btn { - padding: 4px; + padding: 0; min-width: auto; + width: 30px; + height: 30px; + display: flex; + align-items: center; + justify-content: center; } #SendSMSModal .sms-recipient-list { From 784b20faebfbb349273859af429edb2c09bb2620 Mon Sep 17 00:00:00 2001 From: Joey Date: Thu, 11 Jun 2026 22:38:54 +1000 Subject: [PATCH 4/4] Started nsw meshcore - civilial mesh coms layer --- src/pages/tasking/main.js | 2 ++ src/pages/tasking/mapLayers/civilian.js | 46 +++++++++++++++++++++++++ static/manifest.json | 3 +- 3 files changed, 50 insertions(+), 1 deletion(-) create mode 100644 src/pages/tasking/mapLayers/civilian.js diff --git a/src/pages/tasking/main.js b/src/pages/tasking/main.js index bd76d098..8a350e3c 100644 --- a/src/pages/tasking/main.js +++ b/src/pages/tasking/main.js @@ -90,6 +90,7 @@ import { registerBOMFloodWarningBoundariesLayer, registerBOMFireWeatherDistrictsLayer } from "./mapLayers/weather.js"; +import { registerNswMeshNodesLayer } from "./mapLayers/civilian.js" import { fetchHqDetailsSummary } from './utils/hqSummary.js'; @@ -2971,6 +2972,7 @@ function VM() { registerBOMFloodWarningBoundariesLayer(self, sourceUrl); registerBOMFireWeatherDistrictsLayer(self, sourceUrl); registerRainRadarLayer(self, map); + registerNswMeshNodesLayer(self) // --- Layers Drawer (under zoom) const LayersDrawer = L.Control.extend({ diff --git a/src/pages/tasking/mapLayers/civilian.js b/src/pages/tasking/mapLayers/civilian.js new file mode 100644 index 00000000..bc1a4323 --- /dev/null +++ b/src/pages/tasking/mapLayers/civilian.js @@ -0,0 +1,46 @@ +import L from "leaflet"; + + + +export function registerNswMeshNodesLayer(vm) { + vm.mapVM.registerPollingLayer("nswMeshNodes", { + label: "NSW Mesh Nodes", + menuGroup: "Civilian", + visibleByDefault: localStorage.getItem(`ov.nswMeshNodes`) || false, + fetchFn: async () => { + const response = await fetch("https://corescope.nswmesh.au/api/nodes?limit=10000&lastHeard=30d") + if (!response.ok) { throw new Error(`Failed to get MeshCore nodes: ${response.status}`); } + const result = await response.json(); + return result; + }, + drawFn: (layerGroup, data) => { + + if (!data || !Array.isArray(data.nodes)) return; + + + data.nodes.forEach((f) => { + if (!Number.isFinite(f.lat) || !Number.isFinite(f.lon)) return; + + const roleToIconMap = { + repeater : "🛜", + companion : "📟", + observer : "🖥️" + } + + const roleRmoji = roleToIconMap[f.role] ?? "📻" + + const marker = L.marker([f.lat, f.lon], { + icon: L.divIcon({ + html: roleRmoji, + iconSize: [0, 0] + })}); + layerGroup.addLayer(marker); + }); + + + + return; + + }, + }); +} \ No newline at end of file diff --git a/static/manifest.json b/static/manifest.json index 198e9076..d2c30235 100644 --- a/static/manifest.json +++ b/static/manifest.json @@ -40,7 +40,8 @@ "https://nula.waternsw.com.au/*", "https://services1.arcgis.com/*", "https://api.rainviewer.com/*", - "https://portal.spatial.nsw.gov.au/*" + "https://portal.spatial.nsw.gov.au/*", + "https://corescope.nswmesh.au/*" ], "permissions": [ "storage",