@@ -19,14 +19,15 @@ L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {
1919// ---------------------------------------------------------------------------
2020// Marker icon factory
2121// ---------------------------------------------------------------------------
22- function makeIcon ( color ) {
22+ function makeIcon ( color , pulse ) {
2323 const svg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 36">
2424 <path d="M12 0C5.373 0 0 5.373 0 12c0 9 12 24 12 24S24 21 24 12C24 5.373 18.627 0 12 0z"
2525 fill="${ color } " stroke="#fff" stroke-width="1.5"/>
2626 <circle cx="12" cy="12" r="4.5" fill="#fff"/>
2727 </svg>` ;
28+ const cls = pulse ? ' class="marker-pulse"' : '' ;
2829 return L . divIcon ( {
29- html : `<div style="width:24px;height:36px">${ svg } </div>` ,
30+ html : `<div${ cls } style="width:24px;height:36px">${ svg } </div>` ,
3031 iconSize : [ 24 , 36 ] ,
3132 iconAnchor : [ 12 , 36 ] ,
3233 popupAnchor : [ 0 , - 36 ] ,
@@ -35,10 +36,12 @@ function makeIcon(color) {
3536}
3637
3738const ICONS = {
38- active : makeIcon ( "#2ecc71" ) ,
39- planned : makeIcon ( "#f0a500" ) ,
40- other : makeIcon ( "#888888" ) ,
41- search : makeIcon ( "#e74c3c" ) ,
39+ active : makeIcon ( "#2ecc71" ) ,
40+ planned : makeIcon ( "#f0a500" ) ,
41+ other : makeIcon ( "#888888" ) ,
42+ search : makeIcon ( "#e74c3c" ) ,
43+ medium : makeIcon ( "#ff8c00" ) ,
44+ critical : makeIcon ( "#e74c3c" , true ) ,
4245} ;
4346
4447function iconForStatus ( status ) {
@@ -85,6 +88,19 @@ function renderDetail(locId, detail) {
8588
8689 let html = "" ;
8790
91+ // NOC alert banner
92+ if ( detail . alert && detail . alert . level !== "ok" ) {
93+ const lvl = detail . alert . level ;
94+ const icon = lvl === "critical" ? "🔴" : "🟠" ;
95+ html += `<div class="alert-banner alert-${ escHtml ( lvl ) } ">
96+ <span class="alert-banner-icon">${ icon } </span>
97+ <div>
98+ <div class="alert-banner-level">${ escHtml ( lvl . toUpperCase ( ) ) } </div>
99+ ${ detail . alert . reason ? `<div class="alert-banner-reason">${ escHtml ( detail . alert . reason ) } </div>` : "" }
100+ </div>
101+ </div>` ;
102+ }
103+
88104 // ASNs
89105 if ( detail . asns && detail . asns . length > 0 ) {
90106 const tags = detail . asns
@@ -195,15 +211,21 @@ function groupByCoords(locations) {
195211
196212/**
197213 * Create a marker icon with a small count badge for co-located sites.
214+ * Pass alertLevel ("critical" | "medium") to colour the pin accordingly.
198215 */
199- function makeStackedIcon ( count ) {
216+ function makeStackedIcon ( count , alertLevel ) {
217+ const color = alertLevel === "critical" ? "#e74c3c"
218+ : alertLevel === "medium" ? "#ff8c00"
219+ : "#3388ff" ;
220+ const pulse = alertLevel === "critical" ;
200221 const svg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 36">
201222 <path d="M12 0C5.373 0 0 5.373 0 12c0 9 12 24 12 24S24 21 24 12C24 5.373 18.627 0 12 0z"
202- fill="#3388ff " stroke="#fff" stroke-width="1.5"/>
223+ fill="${ color } " stroke="#fff" stroke-width="1.5"/>
203224 <circle cx="12" cy="12" r="4.5" fill="#fff"/>
204225 </svg>` ;
226+ const cls = pulse ? ' class="marker-pulse"' : '' ;
205227 return L . divIcon ( {
206- html : `<div style="width:24px;height:36px;position:relative">${ svg } <span class="colocated-badge">${ count } </span></div>` ,
228+ html : `<div${ cls } style="width:24px;height:36px;position:relative">${ svg } <span class="colocated-badge">${ count } </span></div>` ,
207229 iconSize : [ 24 , 36 ] ,
208230 iconAnchor : [ 12 , 36 ] ,
209231 popupAnchor : [ 0 , - 36 ] ,
@@ -337,6 +359,14 @@ function addColocatedMarker(locations) {
337359 } ) ;
338360
339361 bindHoverAndLock ( marker ) ;
362+
363+ // Register all location IDs so alert updates can find this marker
364+ const ids = locations . map ( ( l ) => l . id ) ;
365+ for ( const loc of locations ) {
366+ markerByLocId [ loc . id ] = marker ;
367+ colocGroupByLocId [ loc . id ] = ids ;
368+ }
369+
340370 marker . addTo ( markerLayer ) ;
341371}
342372
@@ -347,6 +377,49 @@ const markerLayer = L.layerGroup().addTo(map);
347377let allLocations = [ ] ;
348378const CLUSTER_THRESHOLD = 100 ; // Use clustering if more than 100 locations
349379
380+ // ---------------------------------------------------------------------------
381+ // NOC alert marker registry
382+ // Keeps track of every rendered marker so icons can be updated after
383+ // device-detail loads reveal the site's alert level.
384+ // ---------------------------------------------------------------------------
385+ /** locId → L.marker */
386+ const markerByLocId = { } ;
387+ /** locId → array of all locIds sharing the same co-located marker */
388+ const colocGroupByLocId = { } ;
389+ /** locId → "critical" | "medium" | "ok" */
390+ const locationAlerts = { } ;
391+
392+ /**
393+ * Called after device details are loaded for a location.
394+ * Updates the map-marker icon to reflect the computed alert level,
395+ * and (for co-located groups) promotes to the highest level across
396+ * all members that have been loaded so far.
397+ */
398+ function updateMarkerForAlert ( locId , alertLevel ) {
399+ locationAlerts [ locId ] = alertLevel ;
400+ const marker = markerByLocId [ locId ] ;
401+ if ( ! marker ) return ;
402+
403+ const groupIds = colocGroupByLocId [ locId ] ;
404+ if ( groupIds ) {
405+ // Co-located marker: pick the worst level across the known group
406+ const groupLevel = groupIds . reduce ( ( highest , id ) => {
407+ const lvl = locationAlerts [ id ] || "ok" ;
408+ if ( lvl === "critical" ) return "critical" ;
409+ if ( lvl === "medium" && highest !== "critical" ) return "medium" ;
410+ return highest ;
411+ } , "ok" ) ;
412+ if ( groupLevel !== "ok" ) {
413+ marker . setIcon ( makeStackedIcon ( groupIds . length , groupLevel ) ) ;
414+ }
415+ return ;
416+ }
417+
418+ // Single marker
419+ if ( alertLevel === "critical" ) marker . setIcon ( ICONS . critical ) ;
420+ else if ( alertLevel === "medium" ) marker . setIcon ( ICONS . medium ) ;
421+ }
422+
350423async function loadLocations ( ) {
351424 showLoading ( true , "Loading locations from Nautobot…" ) ;
352425 try {
@@ -367,6 +440,9 @@ async function loadLocations() {
367440
368441function renderMarkers ( locations , searchMarker ) {
369442 markerLayer . clearLayers ( ) ;
443+ // Clear registry so stale references don't linger after a re-render
444+ for ( const key of Object . keys ( markerByLocId ) ) delete markerByLocId [ key ] ;
445+ for ( const key of Object . keys ( colocGroupByLocId ) ) delete colocGroupByLocId [ key ] ;
370446
371447 // Add search point marker if provided
372448 if ( searchMarker ) {
@@ -573,6 +649,7 @@ function addMarker(loc) {
573649 } ) ;
574650
575651 bindHoverAndLock ( marker ) ;
652+ markerByLocId [ loc . id ] = marker ;
576653 marker . addTo ( markerLayer ) ;
577654}
578655
@@ -589,6 +666,9 @@ async function fetchAndRenderDetail(locId) {
589666 if ( ! resp . ok ) throw new Error ( `HTTP ${ resp . status } ` ) ;
590667 const detail = await resp . json ( ) ;
591668 renderDetail ( locId , detail ) ;
669+ if ( detail . alert ) {
670+ updateMarkerForAlert ( locId , detail . alert . level ) ;
671+ }
592672 } catch ( err ) {
593673 const container = document . getElementById ( `popup-detail-${ locId } ` ) ;
594674 if ( container ) {
0 commit comments