2026-05-18 17:11:36 +02:00
from __future__ import annotations
def graph_explorer_page ( ) - > str :
return """ <!doctype html>
< html lang = " en " >
< head >
< meta charset = " utf-8 " >
< meta name = " viewport " content = " width=device-width, initial-scale=1 " >
< title > Fabric Map < / title >
< style >
: root {
color - scheme : light ;
font - family : Inter , ui - sans - serif , system - ui , - apple - system , BlinkMacSystemFont , " Segoe UI " , sans - serif ;
- - bg : #f7f8fa;
- - panel : #ffffff;
- - line : #d6dce5;
- - text : #172033;
- - muted : #667085;
- - accent : #0f766e;
- - accent - 2 : #2563eb;
- - warn : #b45309;
}
* { box - sizing : border - box ; }
html , body { height : 100 % ; margin : 0 ; }
body { background : var ( - - bg ) ; color : var ( - - text ) ; font - size : 14 px ; }
. shell { display : grid ; grid - template - rows : auto 1 fr ; height : 100 vh ; min - height : 620 px ; }
. toolbar {
display : grid ;
2026-05-18 23:58:58 +02:00
grid - template - columns : minmax ( 180 px , 1.2 fr ) repeat ( 7 , minmax ( 108 px , .55 fr ) ) auto ;
2026-05-18 17:11:36 +02:00
gap : 8 px ;
align - items : center ;
padding : 10 px 12 px ;
border - bottom : 1 px solid var ( - - line ) ;
background : var ( - - panel ) ;
}
. main { display : grid ; grid - template - columns : minmax ( 0 , 1 fr ) 340 px ; min - height : 0 ; }
. canvas - wrap { position : relative ; min - width : 0 ; min - height : 0 ; }
2026-05-24 17:02:12 +02:00
#graph-canvas { position: absolute; inset: 0; z-index: 0; }
. zone - overlay {
position : absolute ;
inset : 0 ;
z - index : 2 ;
overflow : hidden ;
pointer - events : none ;
}
. zone - boundary {
position : absolute ;
border : 2 px solid var ( - - zone - color , #2563eb);
border - radius : 8 px ;
background : var ( - - zone - fill , rgba ( 37 , 99 , 235 , .06 ) ) ;
box - shadow : inset 0 0 0 1 px rgba ( 255 , 255 , 255 , .74 ) , 0 10 px 32 px rgba ( 23 , 32 , 51 , .08 ) ;
min - height : 56 px ;
min - width : 90 px ;
pointer - events : none ;
}
. zone - boundary . selected {
border - width : 3 px ;
box - shadow : inset 0 0 0 1 px rgba ( 255 , 255 , 255 , .82 ) , 0 14 px 36 px rgba ( 23 , 32 , 51 , .14 ) ;
}
. zone - label {
position : absolute ;
top : 8 px ;
left : 10 px ;
z - index : 1 ;
min - height : 26 px ;
max - width : calc ( 100 % - 20 px ) ;
border - color : var ( - - zone - color , #2563eb);
color : var ( - - zone - color , #2563eb);
background : rgba ( 255 , 255 , 255 , .96 ) ;
box - shadow : 0 8 px 18 px rgba ( 23 , 32 , 51 , .1 ) ;
font - size : 12 px ;
font - weight : 700 ;
overflow : hidden ;
padding : 3 px 8 px ;
pointer - events : auto ;
text - overflow : ellipsis ;
}
. zone - label : focus - visible {
outline : 2 px solid var ( - - zone - color , #2563eb);
outline - offset : 2 px ;
}
2026-05-18 17:11:36 +02:00
. side {
border - left : 1 px solid var ( - - line ) ;
background : var ( - - panel ) ;
min - height : 0 ;
overflow : auto ;
padding : 14 px ;
}
. side h1 { font - size : 17 px ; line - height : 1.2 ; margin : 0 0 10 px ; }
. section { border - top : 1 px solid var ( - - line ) ; padding - top : 12 px ; margin - top : 12 px ; }
. section : first - child { border - top : 0 ; padding - top : 0 ; margin - top : 0 ; }
2026-05-18 23:58:58 +02:00
label , . field { color : var ( - - muted ) ; display : grid ; gap : 4 px ; font - size : 12 px ; }
2026-05-18 17:11:36 +02:00
input , select , button {
border : 1 px solid var ( - - line ) ;
border - radius : 6 px ;
color : var ( - - text ) ;
font : inherit ;
min - height : 34 px ;
padding : 6 px 8 px ;
background : #ffffff;
}
button {
cursor : pointer ;
display : inline - flex ;
align - items : center ;
justify - content : center ;
gap : 6 px ;
white - space : nowrap ;
}
button . primary { background : var ( - - accent ) ; border - color : var ( - - accent ) ; color : #ffffff; }
button : disabled { color : #98a2b3; cursor: default; }
2026-05-19 00:51:38 +02:00
. field - label {
display : inline - flex ;
align - items : center ;
gap : 5 px ;
}
. help - tip {
width : 18 px ;
height : 18 px ;
min - height : 18 px ;
padding : 0 ;
border - radius : 50 % ;
color : var ( - - muted ) ;
font - size : 11 px ;
line - height : 1 ;
background : #f8fafc;
}
. help - tip : hover , . help - tip : focus - visible {
border - color : var ( - - accent ) ;
color : var ( - - accent ) ;
outline : none ;
}
. help - popup {
position : fixed ;
z - index : 30 ;
display : none ;
max - width : 300 px ;
border : 1 px solid var ( - - line ) ;
border - radius : 8 px ;
background : #ffffff;
box - shadow : 0 16 px 40 px rgba ( 23 , 32 , 51 , .16 ) ;
color : var ( - - text ) ;
padding : 10 px ;
pointer - events : none ;
}
. help - popup strong { display : block ; margin - bottom : 4 px ; }
. help - popup p { margin : 0 ; color : var ( - - muted ) ; font - size : 12 px ; line - height : 1.35 ; }
2026-05-18 23:58:58 +02:00
. filter - menu {
position : relative ;
min - width : 0 ;
}
. filter - menu summary {
border : 1 px solid var ( - - line ) ;
border - radius : 6 px ;
color : var ( - - text ) ;
cursor : pointer ;
list - style : none ;
min - height : 34 px ;
overflow : hidden ;
padding : 6 px 8 px ;
text - overflow : ellipsis ;
white - space : nowrap ;
background : #ffffff;
}
. filter - menu summary : : - webkit - details - marker { display : none ; }
. filter - menu [ open ] summary { border - color : var ( - - accent ) ; }
. check - list {
position : absolute ;
z - index : 8 ;
display : grid ;
gap : 4 px ;
width : max - content ;
min - width : 210 px ;
max - width : 280 px ;
max - height : 300 px ;
overflow : auto ;
margin - top : 4 px ;
padding : 8 px ;
border : 1 px solid var ( - - line ) ;
border - radius : 8 px ;
background : #ffffff;
box - shadow : 0 16 px 40 px rgba ( 23 , 32 , 51 , .14 ) ;
}
. check - list label {
grid - template - columns : auto 1 fr ;
align - items : center ;
color : var ( - - text ) ;
font - size : 13 px ;
gap : 8 px ;
min - height : 26 px ;
}
. check - list input {
min - height : 0 ;
padding : 0 ;
}
. map - controls {
position : absolute ;
top : 12 px ;
right : 12 px ;
z - index : 3 ;
display : flex ;
2026-05-19 00:27:09 +02:00
align - items : end ;
2026-05-19 00:51:38 +02:00
flex - wrap : wrap ;
justify - content : flex - end ;
2026-05-18 23:58:58 +02:00
gap : 6 px ;
2026-05-19 00:51:38 +02:00
max - width : min ( 460 px , calc ( 100 % - 24 px ) ) ;
2026-05-18 23:58:58 +02:00
padding : 6 px ;
border : 1 px solid var ( - - line ) ;
border - radius : 8 px ;
background : rgba ( 255 , 255 , 255 , .94 ) ;
box - shadow : 0 12 px 30 px rgba ( 23 , 32 , 51 , .12 ) ;
}
. map - controls button { min - width : 54 px ; }
2026-05-19 00:27:09 +02:00
. map - control - field {
display : grid ;
gap : 2 px ;
color : var ( - - muted ) ;
font - size : 11 px ;
line - height : 1 ;
}
. map - control - field select {
min - height : 30 px ;
min - width : 84 px ;
padding : 4 px 6 px ;
}
2026-05-24 17:02:12 +02:00
. map - control - field input [ type = " checkbox " ] {
width : 18 px ;
height : 18 px ;
min - height : 0 ;
margin : 5 px 0 ;
padding : 0 ;
accent - color : var ( - - accent ) ;
}
2026-05-19 00:16:57 +02:00
. selection - anchor {
position : absolute ;
z - index : 5 ;
display : none ;
width : 0 ;
height : 0 ;
pointer - events : none ;
transform : translate ( - 50 % , - 100 % ) ;
}
. selection - anchor : : before {
content : " " ;
position : absolute ;
left : - 1 px ;
top : - 36 px ;
width : 2 px ;
height : 28 px ;
border - radius : 999 px ;
background : #111827;
box - shadow : 0 1 px 2 px rgba ( 23 , 32 , 51 , .28 ) ;
}
. selection - anchor : : after {
content : " " ;
position : absolute ;
left : - 6 px ;
top : - 10 px ;
width : 0 ;
height : 0 ;
border - left : 6 px solid transparent ;
border - right : 6 px solid transparent ;
border - top : 10 px solid #111827;
filter : drop - shadow ( 0 1 px 1 px rgba ( 23 , 32 , 51 , .24 ) ) ;
}
. selection - anchor - label {
position : absolute ;
left : 12 px ;
top : - 48 px ;
max - width : 220 px ;
overflow : hidden ;
border : 1 px solid var ( - - line ) ;
border - radius : 8 px ;
background : rgba ( 255 , 255 , 255 , .96 ) ;
box - shadow : 0 12 px 30 px rgba ( 23 , 32 , 51 , .12 ) ;
color : var ( - - text ) ;
font - size : 12 px ;
line - height : 1.25 ;
padding : 6 px 8 px ;
text - overflow : ellipsis ;
white - space : nowrap ;
}
2026-05-18 17:11:36 +02:00
. button - row { display : flex ; flex - wrap : wrap ; gap : 8 px ; }
. meta { color : var ( - - muted ) ; font - size : 12 px ; margin : 0 ; }
. pill {
display : inline - flex ;
align - items : center ;
border : 1 px solid var ( - - line ) ;
border - radius : 999 px ;
color : var ( - - muted ) ;
min - height : 24 px ;
padding : 2 px 8 px ;
margin : 0 4 px 4 px 0 ;
font - size : 12 px ;
}
. detail - list { display : grid ; gap : 6 px ; margin : 8 px 0 0 ; padding : 0 ; list - style : none ; }
. detail - list li { overflow - wrap : anywhere ; }
2026-05-19 01:46:23 +02:00
. orientation - list { gap : 8 px ; }
. orientation - item {
border : 1 px solid var ( - - line ) ;
border - radius : 8 px ;
background : #fbfcfe;
padding : 8 px ;
}
. orientation - label {
color : var ( - - muted ) ;
display : block ;
font - size : 11 px ;
margin - bottom : 3 px ;
text - transform : uppercase ;
}
. orientation - value { color : var ( - - text ) ; line - height : 1.35 ; }
. orientation - path { font - family : ui - monospace , SFMono - Regular , Menlo , Consolas , monospace ; font - size : 12 px ; }
. orientation - warning { border - color : #f59e0b; background: #fffbeb; }
. orientation - good { border - color : #5eead4; background: #f0fdfa; }
2026-05-19 01:16:18 +02:00
. rule - panel {
border : 1 px solid var ( - - line ) ;
border - radius : 8 px ;
background : #fbfcfe;
}
. rule - panel summary {
cursor : pointer ;
list - style : none ;
padding : 10 px ;
}
. rule - panel summary : : - webkit - details - marker { display : none ; }
. rule - editor {
display : grid ;
gap : 8 px ;
padding : 0 10 px 10 px ;
}
. rule - grid {
display : grid ;
grid - template - columns : 1 fr 1 fr ;
gap : 8 px ;
}
. rule - list {
display : grid ;
gap : 8 px ;
margin : 10 px 0 0 ;
padding : 0 ;
list - style : none ;
}
. rule - item {
display : grid ;
gap : 6 px ;
border : 1 px solid var ( - - line ) ;
border - radius : 8 px ;
background : #ffffff;
padding : 8 px ;
}
. rule - title {
align - items : center ;
display : flex ;
gap : 6 px ;
justify - content : space - between ;
}
. rule - actions { display : flex ; flex - wrap : wrap ; gap : 6 px ; }
. rule - actions button { min - height : 28 px ; padding : 4 px 7 px ; }
2026-05-18 17:11:36 +02:00
. popup {
position : absolute ;
z - index : 4 ;
max - width : 260 px ;
display : none ;
border : 1 px solid var ( - - line ) ;
border - radius : 8 px ;
background : #ffffff;
box - shadow : 0 16 px 40 px rgba ( 23 , 32 , 51 , .14 ) ;
padding : 10 px ;
pointer - events : none ;
}
. popup strong { display : block ; margin - bottom : 4 px ; }
. notice { color : var ( - - warn ) ; padding : 14 px ; }
2026-05-19 00:51:38 +02:00
. canvas - notice {
position : absolute ;
inset : 0 ;
display : grid ;
place - items : center ;
color : var ( - - muted ) ;
padding : 24 px ;
text - align : center ;
}
2026-05-18 17:11:36 +02:00
@media ( max - width : 900 px ) {
. shell { min - height : 760 px ; }
. toolbar { grid - template - columns : 1 fr 1 fr ; }
. main { grid - template - columns : 1 fr ; grid - template - rows : minmax ( 420 px , 1 fr ) 330 px ; }
. side { border - left : 0 ; border - top : 1 px solid var ( - - line ) ; }
2026-05-19 00:51:38 +02:00
. map - controls { top : 8 px ; right : 8 px ; max - width : calc ( 100 % - 16 px ) ; }
2026-05-19 01:16:18 +02:00
. rule - grid { grid - template - columns : 1 fr ; }
2026-05-18 17:11:36 +02:00
}
< / style >
< / head >
< body >
< section class = " shell " >
< div class = " toolbar " >
2026-05-19 00:51:38 +02:00
< div class = " field " >
< span class = " field-label " > Search < button type = " button " class = " help-tip " aria - label = " Search help " data - help - title = " Search " data - help = " Search matches labels, ids, repos, node types, edge types, and descriptions. It hides non-matching nodes and edges without redrawing the layout. " > ? < / button > < / span >
< input id = " search " autocomplete = " off " aria - label = " Search graph " >
< / div >
< div class = " field " >
< span class = " field-label " > Mode < button type = " button " class = " help-tip " aria - label = " Mode help " data - help - title = " Mode " data - help = " Modes are predefined map views. Some modes use the selected item as context; changing modes changes which graph entities are visible, but keeps the layout controls separate. " > ? < / button > < / span >
< select id = " mode-select " aria - label = " Graph mode " > < / select >
< / div >
< div class = " field " >
< span class = " field-label " > Layout < button type = " button " class = " help-tip " aria - label = " Layout help " data - help - title = " Layout " data - help = " Layout redraws the map arrangement. Cose uses relationship strength and repo affinity; circle, grid, concentric, and breadthfirst are simpler alternate arrangements. " > ? < / button > < / span >
< select id = " layout-select " aria - label = " Graph layout " >
2026-05-18 18:31:25 +02:00
< option value = " cose " > Cose < / option >
< option value = " concentric " > Concentric < / option >
< option value = " circle " > Circle < / option >
< option value = " grid " > Grid < / option >
< option value = " breadthfirst " > Breadthfirst < / option >
2026-05-19 00:51:38 +02:00
< / select >
< / div >
< div class = " field " > < span class = " field-label " > Node Types < button type = " button " class = " help-tip " aria - label = " Node types help " data - help - title = " Node Types " data - help = " Nodes are the entities drawn on the map: repositories, services, deployments, servers, capabilities, interfaces, dependencies, bindings, and libraries. This filter hides unchecked node types while preserving the current layout. " > ? < / button > < / span >
2026-05-18 23:58:58 +02:00
< details id = " node-type-menu " class = " filter-menu " >
< summary id = " node-type-summary " > All node types < / summary >
< div id = " node-type-filter " class = " check-list " > < / div >
< / details >
< / div >
2026-05-19 00:51:38 +02:00
< div class = " field " > < span class = " field-label " > Edge Types < button type = " button " class = " help-tip " aria - label = " Edge types help " data - help - title = " Edge Types " data - help = " Edges are relationships between nodes, such as provides, consumes, runs_on, or binds. This filter hides unchecked relationship types while preserving the current layout. " > ? < / button > < / span >
2026-05-18 23:58:58 +02:00
< details id = " edge-type-menu " class = " filter-menu " >
< summary id = " edge-type-summary " > All edge types < / summary >
< div id = " edge-type-filter " class = " check-list " > < / div >
< / details >
< / div >
2026-05-19 00:51:38 +02:00
< div class = " field " >
< span class = " field-label " > Review < button type = " button " class = " help-tip " aria - label = " Review help " data - help - title = " Review " data - help = " Review state applies only to entities that carry review metadata. Accepted means trusted data; candidate means registered or inferred data that still needs context. " > ? < / button > < / span >
< select id = " review-filter " aria - label = " Review state filter " > < option value = " " > Any < / option > < option > accepted < / option > < option > candidate < / option > < / select >
< / div >
< div class = " field " >
< span class = " field-label " > Unresolved < button type = " button " class = " help-tip " aria - label = " Unresolved help " data - help - title = " Unresolved " data - help = " Unresolved marks missing provider bindings or registered repositories without accepted graph snapshots. It only applies to node and edge types that can express those gaps. " > ? < / button > < / span >
< select id = " unresolved-filter " aria - label = " Unresolved filter " > < option value = " " > Any < / option > < option value = " true " > Only < / option > < / select >
< / div >
< div class = " field " >
2026-05-19 01:16:18 +02:00
< span class = " field-label " > Profile < button type = " button " class = " help-tip " aria - label = " Saved views help " data - help - title = " Saved Views " data - help = " Saved views store the current search, mode, layout, label density, filters, rules, and manual visibility overrides in this browser. Copy State creates a shareable URL for the current view. " > ? < / button > < / span >
2026-05-19 00:51:38 +02:00
< select id = " profile-select " aria - label = " Saved view profile " disabled > < option > Unsaved exploration < / option > < / select >
< / div >
2026-05-18 17:11:36 +02:00
< span id = " counts " class = " meta " > < / span >
< / div >
< main class = " main " >
< div class = " canvas-wrap " >
2026-05-19 00:51:38 +02:00
< div id = " graph-canvas " role = " img " aria - label = " Interactive Fabric graph " aria - live = " polite " > < p class = " canvas-notice " > Loading graph . . . < / p > < / div >
2026-05-24 17:02:12 +02:00
< div id = " zone-overlay " class = " zone-overlay " aria - label = " Deployment zone boundaries " > < / div >
2026-05-18 23:58:58 +02:00
< div class = " map-controls " aria - label = " Map navigation controls " >
2026-05-24 17:02:12 +02:00
< label class = " map-control-field " > < span class = " field-label " > Zones < button type = " button " class = " help-tip " aria - label = " Zone boundary help " data - help - title = " Zones " data - help = " Zone boundaries draw labeled rectangles around visible nodes that share deployment overlay fields. They are visual annotations only. " > ? < / button > < / span >
< input id = " zone-boundary-toggle " type = " checkbox " aria - label = " Show zone boundaries " checked >
< / label >
< label class = " map-control-field " > < span class = " field-label " > Zone By < button type = " button " class = " help-tip " aria - label = " Zone grouping help " data - help - title = " Zone By " data - help = " Environment groups nodes as dev-tegwick, test, or prod. Access Zone groups by intended reachability such as private-dev or production-public. " > ? < / button > < / span >
< select id = " zone-group-select " title = " Choose zone boundary grouping " >
< option value = " deploymentEnvironment " > Environment < / option >
< option value = " accessZone " > Access Zone < / option >
< / select >
< / label >
2026-05-19 00:51:38 +02:00
< label class = " map-control-field " > < span class = " field-label " > Labels < button type = " button " class = " help-tip " aria - label = " Labels help " data - help - title = " Labels " data - help = " Label density changes only text visibility. Auto hides low-priority labels when the map is dense; Key keeps repositories, services, deployments, servers, and issue markers visible. " > ? < / button > < / span >
2026-05-19 00:27:09 +02:00
< select id = " label-select " title = " Control node label density " >
< option value = " auto " > Auto < / option >
< option value = " key " > Key < / option >
< option value = " all " > All < / option >
< option value = " none " > None < / option >
< / select >
< / label >
2026-05-19 00:51:38 +02:00
< button type = " button " data - action = " fit " title = " Fit graph to view " data - help - title = " Fit " data - help = " Fit pans and zooms the current visible graph into the map viewport. It does not change filters, labels, or layout. " > Fit < / button >
< button type = " button " data - action = " focus " title = " Focus selected neighborhood " data - help - title = " Focus " data - help = " Focus narrows the map to the selected entity and its nearby context. It changes visibility, not the underlying registry data. " > Focus < / button >
< button type = " button " data - action = " clear " class = " primary " title = " Reset map controls " data - help - title = " Reset " data - help = " Reset clears search, mode, node and edge type filters, label density, unresolved/review filters, focus, and manual overrides. " > Reset < / button >
2026-05-18 23:58:58 +02:00
< / div >
2026-05-19 00:16:57 +02:00
< div id = " selection-anchor " class = " selection-anchor " aria - hidden = " true " >
< span id = " selection-anchor-label " class = " selection-anchor-label " > < / span >
< / div >
2026-05-18 17:11:36 +02:00
< div id = " popup " class = " popup " > < / div >
2026-05-19 00:51:38 +02:00
< div id = " help-popup " class = " help-popup " role = " tooltip " > < / div >
2026-05-18 17:11:36 +02:00
< / div >
< aside class = " side " >
< section class = " section " >
< h1 id = " detail-title " > Fabric Map < / h1 >
< p id = " detail-summary " class = " meta " > No selection < / p >
< div id = " detail-pills " > < / div >
< ul id = " detail-list " class = " detail-list " > < / ul >
< / section >
2026-05-18 18:36:15 +02:00
< section class = " section " >
< p id = " orientation-title " class = " meta " > Select a service , interface , dependency , or registered - only repo . < / p >
2026-05-19 01:46:23 +02:00
< ul id = " orientation-list " class = " detail-list orientation-list " > < / ul >
< div id = " orientation-actions " class = " button-row " > < / div >
2026-05-18 18:36:15 +02:00
< / section >
2026-05-18 17:11:36 +02:00
< section class = " section " >
< div class = " button-row " >
2026-05-19 00:51:38 +02:00
< button type = " button " data - override = " show " data - help - title = " Show " data - help = " Show pins the selected entity visible even when other filters would hide it. It does not redraw the layout. " > Show < / button >
< button type = " button " data - override = " blur " data - help - title = " Blur " data - help = " Blur keeps the selected entity in place but reduces emphasis. It is useful for context that should stay visible without competing for attention. " > Blur < / button >
< button type = " button " data - override = " hide " data - help - title = " Hide " data - help = " Hide makes the selected entity invisible while preserving layout context. Remove is reserved for the upcoming rule panel and will redraw the graph without the entity. " > Hide < / button >
< button type = " button " data - action = " reset-overrides " data - help - title = " Clear Overrides " data - help = " Clear Overrides removes manual Show, Blur, and Hide choices. Filters, mode, layout, and label density stay as they are. " > Clear Overrides < / button >
2026-05-18 17:11:36 +02:00
< / div >
< / section >
2026-05-19 01:16:18 +02:00
< section class = " section " >
< details id = " rule-panel " class = " rule-panel " >
< summary > < span class = " field-label " > Rules < button type = " button " class = " help-tip " aria - label = " Rule builder help " data - help - title = " Rule Builder " data - help = " Rules select nodes or edges by type and available attributes, then apply a modifier. Hide preserves the current layout; Remove redraws the map without matching entities. " > ? < / button > < / span > < / summary >
< div class = " rule-editor " >
< div class = " rule-grid " >
< label > Target
< select id = " rule-target " >
< option value = " node " > Nodes < / option >
< option value = " edge " > Edges < / option >
< / select >
< / label >
< label > Type
< select id = " rule-type " > < / select >
< / label >
< label > Attribute
< select id = " rule-attribute " > < / select >
< / label >
< label > Value
< select id = " rule-value " > < / select >
< / label >
< label > Modifier
< select id = " rule-action " >
< option value = " highlight " > Highlight < / option >
< option value = " show " > Show < / option >
< option value = " blur " > Blur < / option >
< option value = " hide " > Hide < / option >
< option value = " remove " > Remove and redraw < / option >
< / select >
< / label >
< / div >
< p id = " rule-context " class = " meta " > Rules are applied top to bottom ; later matching rules refine earlier ones . < / p >
< div class = " button-row " >
< button type = " button " data - rule - action = " save " class = " primary " > Add Rule < / button >
< button type = " button " data - rule - action = " cancel " > Cancel Edit < / button >
< button type = " button " data - rule - action = " reset " > Reset Rules < / button >
< / div >
< ul id = " rule-list " class = " rule-list " > < / ul >
< / div >
< / details >
< / section >
2026-05-18 18:31:25 +02:00
< section class = " section " >
< p id = " profile-summary " class = " meta " > Profile persistence unavailable for this host . < / p >
2026-05-19 00:51:38 +02:00
< div class = " field " >
< span class = " field-label " > Profile Name < button type = " button " class = " help-tip " aria - label = " Profile name help " data - help - title = " Profile Name " data - help = " Profile Name labels a saved browser-local map view. It does not rename graph entities or registry data. " > ? < / button > < / span >
< input id = " profile-name " autocomplete = " off " aria - label = " Profile name " disabled >
< / div >
2026-05-18 18:31:25 +02:00
< div class = " button-row " >
2026-05-19 01:16:18 +02:00
< button type = " button " data - profile - action = " save " data - help - title = " Save " data - help = " Save stores the current browser-local view, including filters, rules, layout, label density, and manual overrides. " disabled > Save < / button >
2026-05-19 00:51:38 +02:00
< button type = " button " data - profile - action = " duplicate " data - help - title = " Duplicate " data - help = " Duplicate creates a new saved view from the currently loaded profile and current map state. " disabled > Duplicate < / button >
< button type = " button " data - profile - action = " delete " data - help - title = " Delete " data - help = " Delete removes the selected browser-local saved view. It does not delete graph data. " disabled > Delete < / button >
2026-05-19 01:16:18 +02:00
< button type = " button " data - profile - action = " copy " data - help - title = " Copy State " data - help = " Copy State copies a URL that can restore this map state, including filters, rules, and manual overrides. " disabled > Copy State < / button >
2026-05-18 18:31:25 +02:00
< / div >
< / section >
2026-05-18 17:11:36 +02:00
< section class = " section " >
2026-05-19 00:51:38 +02:00
< p class = " meta " > < span id = " hidden-summary " > Hidden 0 < / span > < button type = " button " class = " help-tip " aria - label = " Hidden entities help " data - help - title = " Hidden Entities " data - help = " Hidden counts entities currently not shown because of filters, mode, or manual Hide overrides. Hidden keeps layout context; Remove will redraw without the entity in the rule panel. " > ? < / button > < / p >
2026-05-18 17:11:36 +02:00
< div id = " legend " > < / div >
< / section >
< / aside >
< / main >
< / section >
< script src = " https://unpkg.com/cytoscape@3.28.1/dist/cytoscape.min.js " > < / script >
< script >
( ( ) = > {
const manifestUrl = " /exports/graph-explorer/manifest " ;
const graphUrl = " /exports/graph-explorer " ;
const canvas = document . getElementById ( " graph-canvas " ) ;
2026-05-24 17:02:12 +02:00
const zoneOverlay = document . getElementById ( " zone-overlay " ) ;
2026-05-18 17:11:36 +02:00
const popup = document . getElementById ( " popup " ) ;
2026-05-19 00:51:38 +02:00
const helpPopup = document . getElementById ( " help-popup " ) ;
2026-05-19 00:16:57 +02:00
const selectionAnchor = document . getElementById ( " selection-anchor " ) ;
const selectionAnchorLabel = document . getElementById ( " selection-anchor-label " ) ;
2026-05-18 17:11:36 +02:00
const searchInput = document . getElementById ( " search " ) ;
2026-05-18 18:31:25 +02:00
const modeSelect = document . getElementById ( " mode-select " ) ;
const layoutSelect = document . getElementById ( " layout-select " ) ;
2026-05-19 00:27:09 +02:00
const labelSelect = document . getElementById ( " label-select " ) ;
2026-05-24 17:02:12 +02:00
const zoneBoundaryToggle = document . getElementById ( " zone-boundary-toggle " ) ;
const zoneGroupSelect = document . getElementById ( " zone-group-select " ) ;
2026-05-18 23:58:58 +02:00
const nodeTypeFilter = document . getElementById ( " node-type-filter " ) ;
const nodeTypeSummary = document . getElementById ( " node-type-summary " ) ;
const edgeTypeFilter = document . getElementById ( " edge-type-filter " ) ;
const edgeTypeSummary = document . getElementById ( " edge-type-summary " ) ;
2026-05-18 17:11:36 +02:00
const reviewFilter = document . getElementById ( " review-filter " ) ;
const unresolvedFilter = document . getElementById ( " unresolved-filter " ) ;
2026-05-19 01:16:18 +02:00
const ruleTarget = document . getElementById ( " rule-target " ) ;
const ruleType = document . getElementById ( " rule-type " ) ;
const ruleAttribute = document . getElementById ( " rule-attribute " ) ;
const ruleValue = document . getElementById ( " rule-value " ) ;
const ruleAction = document . getElementById ( " rule-action " ) ;
const ruleContext = document . getElementById ( " rule-context " ) ;
const ruleList = document . getElementById ( " rule-list " ) ;
2026-05-18 18:31:25 +02:00
const profileSelect = document . getElementById ( " profile-select " ) ;
2026-05-18 20:14:07 +02:00
const profileNameInput = document . getElementById ( " profile-name " ) ;
2026-05-18 18:31:25 +02:00
const profileSummary = document . getElementById ( " profile-summary " ) ;
2026-05-18 17:11:36 +02:00
const counts = document . getElementById ( " counts " ) ;
const hiddenSummary = document . getElementById ( " hidden-summary " ) ;
const detailTitle = document . getElementById ( " detail-title " ) ;
const detailSummary = document . getElementById ( " detail-summary " ) ;
const detailPills = document . getElementById ( " detail-pills " ) ;
const detailList = document . getElementById ( " detail-list " ) ;
2026-05-18 18:36:15 +02:00
const orientationTitle = document . getElementById ( " orientation-title " ) ;
const orientationList = document . getElementById ( " orientation-list " ) ;
2026-05-19 01:46:23 +02:00
const orientationActions = document . getElementById ( " orientation-actions " ) ;
2026-05-18 17:11:36 +02:00
const legend = document . getElementById ( " legend " ) ;
2026-05-18 20:14:07 +02:00
const profileStorageKey = " railiance.fabric.graphExplorer.profiles " ;
2026-05-18 17:11:36 +02:00
let cy = null ;
let selected = null ;
2026-05-19 00:16:57 +02:00
let selectedAnchor = null ;
2026-05-18 17:11:36 +02:00
let focusSet = null ;
let manualOverrides = { } ;
let layerColors = { } ;
2026-05-18 23:58:58 +02:00
let nodeTypeLabels = { } ;
let allNodeTypes = [ ] ;
let allEdgeTypes = [ ] ;
2026-05-18 18:31:25 +02:00
let activeMode = " full " ;
2026-05-19 00:27:09 +02:00
let activeLabelMode = " auto " ;
2026-05-24 17:02:12 +02:00
let activeZoneGrouping = " deploymentEnvironment " ;
2026-05-18 20:14:07 +02:00
let profilePersistence = " none " ;
let profiles = [ ] ;
let currentProfileId = " " ;
2026-05-19 01:16:18 +02:00
let filterRules = [ ] ;
let editingRuleId = " " ;
2026-05-19 01:46:23 +02:00
let orientationContext = null ;
2026-05-24 17:02:12 +02:00
let selectedZoneId = " " ;
let zoneOverlayFrame = 0 ;
let zoneSummaries = new Map ( ) ;
2026-05-18 17:11:36 +02:00
const escapeHtml = ( value ) = > String ( value ? ? " " )
. replaceAll ( " & " , " & " ) . replaceAll ( " < " , " < " )
. replaceAll ( " > " , " > " ) . replaceAll ( ' " ' , " " " ) ;
2026-05-19 00:51:38 +02:00
const showHelp = ( target ) = > {
if ( ! helpPopup | | ! target ? . dataset ? . help ) return ;
const bounds = target . getBoundingClientRect ( ) ;
const title = target . dataset . helpTitle | | target . textContent | | " Help " ;
helpPopup . innerHTML = ` < strong > $ { escapeHtml ( title ) } < / strong > < p > $ { escapeHtml ( target . dataset . help ) } < / p > ` ;
helpPopup . style . display = " block " ;
const width = Math . min ( helpPopup . offsetWidth | | 300 , 300 ) ;
const left = Math . min ( Math . max ( 12 , bounds . left ) , window . innerWidth - width - 12 ) ;
const top = bounds . bottom + 8 < window . innerHeight - 80
? bounds . bottom + 8
: Math . max ( 12 , bounds . top - helpPopup . offsetHeight - 8 ) ;
helpPopup . style . left = ` $ { left } px ` ;
helpPopup . style . top = ` $ { top } px ` ;
} ;
const hideHelp = ( ) = > {
if ( ! helpPopup ) return ;
helpPopup . style . display = " none " ;
} ;
2026-05-18 17:11:36 +02:00
const elementText = ( data ) = > [
data . id , data . stableKey , data . label , data . name , data . description ,
2026-05-24 15:55:05 +02:00
data . repo , data . domain , data . kind , data . layer , data . edgeType ,
data . deploymentEnvironment , data . deploymentScenario , data . routingAuthority ,
data . accessZone , data . policyAuthority , data . exposureClass ,
data . routeHost , data . routeHostname , data . routePort , data . routeProtocol , data . route
2026-05-18 17:11:36 +02:00
] . join ( " " ) . toLowerCase ( ) ;
2026-05-18 20:14:07 +02:00
const overrideKey = ( element ) = > element . data ( " stableKey " ) | | element . id ( ) ;
const manualOverrideFor = ( element ) = >
manualOverrides [ overrideKey ( element ) ] | | manualOverrides [ element . id ( ) ] ;
const hasManualOverrides = ( ) = > Object . keys ( manualOverrides ) . length > 0 ;
const supportsLocalProfiles = ( ) = > profilePersistence == = " local " ;
2026-05-18 23:58:58 +02:00
const humanize = ( value ) = > String ( value | | " " )
. replaceAll ( " _ " , " " )
. replaceAll ( " : " , " : " )
. replace ( / \\b \\w / g , ( letter ) = > letter . toUpperCase ( ) ) ;
const parseListParam = ( value ) = > String ( value | | " " )
. split ( " , " )
. map ( ( item ) = > item . trim ( ) )
. filter ( Boolean ) ;
const checkedValues = ( container ) = > new Set (
Array . from ( container . querySelectorAll ( " input[type= ' checkbox ' ] " ) )
. filter ( ( input ) = > input . checked )
. map ( ( input ) = > input . value )
) ;
const setCheckedValues = ( container , values = null ) = > {
const explicit = Array . isArray ( values ) ;
const selectedValues = new Set ( values | | [ ] ) ;
container . querySelectorAll ( " input[type= ' checkbox ' ] " ) . forEach ( ( input ) = > {
input . checked = explicit ? selectedValues . has ( input . value ) : true ;
} ) ;
} ;
const selectedNodeTypes = ( ) = > checkedValues ( nodeTypeFilter ) ;
const selectedEdgeTypes = ( ) = > checkedValues ( edgeTypeFilter ) ;
2026-05-24 15:55:05 +02:00
const hasValue = ( value ) = > value != = undefined & & value != = null & & value != = " " ;
const overlayRuleAttributes = [
" deploymentEnvironment " ,
" deploymentScenario " ,
" routingAuthority " ,
" accessZone " ,
" policyAuthority " ,
" exposureClass " ,
" routeHost " ,
" routeHostname " ,
" routePort " ,
" routeProtocol " ,
] ;
const zoneModeFields = {
" by-fabric " : [ " domain " , " repo " , " kind " ] ,
" by-deployment-environment " : [ " deploymentEnvironment " ] ,
" by-deployment-scenario " : [ " deploymentScenario " ] ,
" by-routing-authority " : [ " routingAuthority " ] ,
" by-access-zone " : [ " accessZone " ] ,
} ;
const zoneModeLabels = {
" by-fabric " : " Fabric " ,
" by-deployment-environment " : " Deployment Environment " ,
" by-deployment-scenario " : " Deployment Scenario " ,
" by-routing-authority " : " Routing Authority " ,
" by-access-zone " : " Access Zone " ,
} ;
2026-05-18 23:58:58 +02:00
const summarizeSelection = ( selected , allValues , labels , noun ) = > {
if ( selected . size == = 0 ) return ` No $ { noun } ` ;
if ( selected . size == = allValues . length ) return ` All $ { noun } ` ;
const names = allValues
. filter ( ( value ) = > selected . has ( value ) )
. map ( ( value ) = > labels [ value ] | | humanize ( value ) ) ;
return names . length < = 2 ? names . join ( " , " ) : ` $ { names . length } $ { noun } ` ;
} ;
const syncFilterSummaries = ( ) = > {
nodeTypeSummary . textContent = summarizeSelection (
selectedNodeTypes ( ) , allNodeTypes , nodeTypeLabels , " node types "
) ;
edgeTypeSummary . textContent = summarizeSelection (
selectedEdgeTypes ( ) , allEdgeTypes , { } , " edge types "
) ;
} ;
const renderChecklist = ( container , values , labels , name ) = > {
container . innerHTML = values . map ( ( value ) = > {
const label = labels [ value ] | | humanize ( value ) ;
return ` < label > < input type = " checkbox " name = " $ { escapeHtml(name)} " value = " $ { escapeHtml(value)} " checked > $ { escapeHtml ( label ) } < / label > ` ;
} ) . join ( " " ) ;
} ;
2026-05-19 01:16:18 +02:00
const ruleActionLabels = {
show : " Show " ,
hide : " Hide " ,
blur : " Blur " ,
highlight : " Highlight " ,
remove : " Remove and redraw " ,
} ;
const ruleAttributeLabels = {
any : " Any attribute " ,
repo : " Repo " ,
kind : " Kind " ,
reviewState : " Review state " ,
unresolved : " Unresolved " ,
lifecycle : " Lifecycle " ,
strength : " Strength " ,
sameRepo : " Same repo " ,
layoutAffinity : " Layout affinity " ,
2026-05-24 15:55:05 +02:00
deploymentEnvironment : " Deployment Environment " ,
deploymentScenario : " Deployment Scenario " ,
routingAuthority : " Routing Authority " ,
accessZone : " Access Zone " ,
policyAuthority : " Policy Authority " ,
exposureClass : " Exposure Class " ,
routeHost : " Route Host " ,
routeHostname : " Route Hostname " ,
routePort : " Route Port " ,
routeProtocol : " Route Protocol " ,
2026-05-19 01:16:18 +02:00
} ;
const ruleAttributeCandidates = {
2026-05-24 15:55:05 +02:00
node : [ " any " , " repo " , " kind " , " reviewState " , " unresolved " , " lifecycle " , . . . overlayRuleAttributes ] ,
edge : [ " any " , " repo " , " kind " , " reviewState " , " unresolved " , " strength " , " sameRepo " , " layoutAffinity " , . . . overlayRuleAttributes ] ,
2026-05-19 01:16:18 +02:00
} ;
const renderOptions = ( select , options , current = " " ) = > {
select . innerHTML = options . map ( ( option ) = >
` < option value = " $ { escapeHtml(option.value)} " > $ { escapeHtml ( option . label ) } < / option > `
) . join ( " " ) ;
if ( options . some ( ( option ) = > option . value == = current ) ) select . value = current ;
} ;
const ruleElementType = ( element , target ) = >
target == = " edge " ? element . data ( " edgeType " ) : element . data ( " layer " ) ;
const currentRuleElements = ( ) = > {
if ( ! cy ) return [ ] ;
const target = ruleTarget . value | | " node " ;
const type = ruleType . value | | " " ;
const elements = target == = " edge " ? cy . edges ( ) : cy . nodes ( ) ;
return elements . filter ( ( element ) = > ! type | | ruleElementType ( element , target ) == = type ) . toArray ( ) ;
} ;
const valuesForRuleAttribute = ( attribute ) = > {
if ( attribute == = " any " ) return [ { value : " " , label : " Any " } ] ;
const values = Array . from ( new Set ( currentRuleElements ( )
. map ( ( element ) = > element . data ( attribute ) )
. filter ( ( value ) = > value != = undefined & & value != = null & & value != = " " )
. map ( ( value ) = > String ( value ) )
) ) . sort ( ( left , right ) = > left . localeCompare ( right ) ) ;
return values . length
? values . map ( ( value ) = > ( { value , label : humanize ( value ) } ) )
: [ { value : " " , label : " No values " } ] ;
} ;
const availableRuleAttributes = ( ) = > {
const elements = currentRuleElements ( ) ;
return ( ruleAttributeCandidates [ ruleTarget . value | | " node " ] | | [ " any " ] )
. filter ( ( attribute ) = > attribute == = " any " | | elements . some ( ( element ) = > {
const value = element . data ( attribute ) ;
return value != = undefined & & value != = null & & value != = " " ;
} ) )
. map ( ( attribute ) = > ( { value : attribute , label : ruleAttributeLabels [ attribute ] | | humanize ( attribute ) } ) ) ;
} ;
const refreshRuleBuilder = ( state = { } ) = > {
const target = state . target | | ruleTarget . value | | " node " ;
ruleTarget . value = target ;
const types = target == = " edge " ? allEdgeTypes : allNodeTypes ;
const labels = target == = " edge " ? { } : nodeTypeLabels ;
renderOptions ( ruleType , [
{ value : " " , label : target == = " edge " ? " All edge types " : " All node types " } ,
. . . types . map ( ( type ) = > ( { value : type , label : labels [ type ] | | humanize ( type ) } ) ) ,
] , state . type ? ? ruleType . value ) ;
renderOptions ( ruleAttribute , availableRuleAttributes ( ) , state . attribute ? ? ruleAttribute . value ) ;
renderOptions ( ruleValue , valuesForRuleAttribute ( ruleAttribute . value | | " any " ) , state . value ? ? ruleValue . value ) ;
if ( state . action & & ruleActionLabels [ state . action ] ) ruleAction . value = state . action ;
ruleValue . disabled = ( ruleAttribute . value | | " any " ) == = " any " | | ruleValue . options . length == = 0 ;
const attributeNames = Array . from ( ruleAttribute . options ) . map ( ( option ) = > option . textContent ) . join ( " , " ) ;
ruleContext . textContent = attributeNames
? ` Available here : $ { attributeNames } . Rules are applied top to bottom ; later matches refine earlier ones . `
: " No attributes are available for this type yet. " ;
} ;
const normalizeRules = ( rules ) = > Array . isArray ( rules )
? rules
. filter ( ( rule ) = > rule & & ( rule . target == = " node " | | rule . target == = " edge " ) & & ruleActionLabels [ rule . action ] )
. map ( ( rule , index ) = > ( {
id : String ( rule . id | | ` rule - $ { Date . now ( ) . toString ( 36 ) } - $ { index } ` ) ,
target : rule . target ,
type : String ( rule . type | | " " ) ,
attribute : String ( rule . attribute | | " any " ) ,
value : String ( rule . value | | " " ) ,
action : rule . action ,
} ) )
: [ ] ;
const ruleDescription = ( rule ) = > {
const target = rule . target == = " edge " ? " edges " : " nodes " ;
const typeLabel = rule . type
? ( rule . target == = " edge " ? humanize ( rule . type ) : nodeTypeLabels [ rule . type ] | | humanize ( rule . type ) )
: ` all $ { target } ` ;
const attribute = rule . attribute & & rule . attribute != = " any "
? ` $ { ruleAttributeLabels [ rule . attribute ] | | humanize ( rule . attribute ) } = $ { humanize ( rule . value ) } `
: " any attribute " ;
return ` $ { ruleActionLabels [ rule . action ] } $ { typeLabel } where $ { attribute } ` ;
} ;
const renderRules = ( ) = > {
const saveButton = document . querySelector ( " [data-rule-action= ' save ' ] " ) ;
const cancelButton = document . querySelector ( " [data-rule-action= ' cancel ' ] " ) ;
saveButton . textContent = editingRuleId ? " Update Rule " : " Add Rule " ;
cancelButton . disabled = ! editingRuleId ;
ruleList . innerHTML = filterRules . length
? filterRules . map ( ( rule , index ) = > `
< li class = " rule-item " data - rule - id = " $ { escapeHtml(rule.id)} " >
< div class = " rule-title " > < strong > $ { escapeHtml ( ruleDescription ( rule ) ) } < / strong > < span class = " meta " > $ { index + 1 } < / span > < / div >
< div class = " rule-actions " >
< button type = " button " data - rule - list - action = " up " $ { index == = 0 ? " disabled " : " " } > Up < / button >
< button type = " button " data - rule - list - action = " down " $ { index == = filterRules . length - 1 ? " disabled " : " " } > Down < / button >
< button type = " button " data - rule - list - action = " edit " > Edit < / button >
< button type = " button " data - rule - list - action = " delete " > Delete < / button >
< / div >
< / li >
` ) . join ( " " )
: ' <li class= " meta " >No rules yet.</li> ' ;
} ;
const matchesRule = ( element , rule ) = > {
if ( rule . target == = " node " & & ! element . isNode ( ) ) return false ;
if ( rule . target == = " edge " & & ! element . isEdge ( ) ) return false ;
if ( rule . type & & ruleElementType ( element , rule . target ) != = rule . type ) return false ;
if ( ! rule . attribute | | rule . attribute == = " any " ) return true ;
const value = element . data ( rule . attribute ) ;
return value != = undefined & & value != = null & & String ( value ) == = String ( rule . value ) ;
} ;
const ruleActionFor = ( element ) = > {
let action = " " ;
filterRules . forEach ( ( rule ) = > {
if ( matchesRule ( element , rule ) ) action = rule . action ;
} ) ;
return action ;
} ;
const ruleRemovalSignature = ( ) = > cy
? cy . elements ( )
. filter ( ( element ) = > element . data ( " displayState " ) == = " remove " )
. map ( ( element ) = > element . id ( ) )
. sort ( )
. join ( " | " )
: " " ;
2026-05-19 00:16:57 +02:00
const isFinitePoint = ( position ) = >
position & & Number . isFinite ( position . x ) & & Number . isFinite ( position . y ) ;
const elementLabel = ( element ) = > {
if ( ! element ) return " " ;
const data = element . data ( ) ;
return data . name | | data . label | | data . edgeType | | data . id ;
} ;
2026-05-19 01:46:23 +02:00
const nodeById = ( id ) = > {
if ( ! cy | | ! id ) return null ;
const node = cy . getElementById ( id ) ;
return node & & node . length > 0 ? node : null ;
} ;
const nodeName = ( id ) = > {
const node = nodeById ( id ) ;
return node ? elementLabel ( node ) : id ;
} ;
const addContextElement = ( ids , element ) = > {
if ( ! element | | element . length == = 0 ) return ;
ids . add ( element . id ( ) ) ;
} ;
const addContextNode = ( ids , id ) = > {
const node = nodeById ( id ) ;
if ( node ) ids . add ( node . id ( ) ) ;
} ;
const addContextEdge = ( ids , edge ) = > {
addContextElement ( ids , edge ) ;
if ( ! edge | | edge . length == = 0 ) return ;
addContextNode ( ids , edge . data ( " source " ) ) ;
addContextNode ( ids , edge . data ( " target " ) ) ;
} ;
const edgeCollection = ( source = " " , target = " " , type = " " ) = > cy ? cy . edges ( ) . filter ( ( edge ) = >
( ! source | | edge . data ( " source " ) == = source ) & &
( ! target | | edge . data ( " target " ) == = target ) & &
( ! type | | edge . data ( " edgeType " ) == = type )
) : [ ] ;
2026-05-24 15:55:05 +02:00
const routeLabel = ( data ) = > {
if ( hasValue ( data . route ) ) return data . route ;
const host = data . routeHost | | data . routeHostname | | " " ;
const protocol = data . routeProtocol | | " " ;
const port = data . routePort | | " " ;
if ( ! hasValue ( host ) & & ! hasValue ( port ) ) return " " ;
const authority = hasValue ( port ) ? ` $ { host } : $ { port } ` : host ;
return [ protocol , authority ] . filter ( hasValue ) . join ( " " ) ;
} ;
const hasRouteEvidence = ( data ) = >
hasValue ( data . route ) | |
hasValue ( data . routeHost ) | |
hasValue ( data . routeHostname ) | |
hasValue ( data . routePort ) ;
const zoneWarningsForData = ( data ) = > {
const warnings = [ ] ;
if ( hasRouteEvidence ( data ) & & ! hasValue ( data . policyAuthority ) ) {
warnings . push ( " route without policy authority " ) ;
}
return warnings ;
} ;
2026-05-24 17:02:12 +02:00
const zonePalette = {
" deploymentEnvironment:dev-tegwick " : { color : " #0f766e " , fill : " rgba(15, 118, 110, .07) " } ,
" deploymentEnvironment:test " : { color : " #2563eb " , fill : " rgba(37, 99, 235, .07) " } ,
" deploymentEnvironment:prod " : { color : " #be123c " , fill : " rgba(190, 18, 60, .07) " } ,
} ;
const fallbackZonePalette = [
{ color : " #7c3aed " , fill : " rgba(124, 58, 237, .07) " } ,
{ color : " #b45309 " , fill : " rgba(180, 83, 9, .08) " } ,
{ color : " #0891b2 " , fill : " rgba(8, 145, 178, .08) " } ,
{ color : " #4f46e5 " , fill : " rgba(79, 70, 229, .07) " } ,
] ;
const hashString = ( value ) = > String ( value | | " " )
. split ( " " )
. reduce ( ( total , character ) = > ( total * 31 + character . charCodeAt ( 0 ) ) >> > 0 , 0 ) ;
const zoneStyle = ( zone ) = >
zonePalette [ zone . id ] | | fallbackZonePalette [ hashString ( zone . id ) % fallbackZonePalette . length ] ;
const zoneRank = ( zone ) = > {
if ( zone . id == = " deploymentEnvironment:dev-tegwick " ) return 0 ;
if ( zone . id == = " deploymentEnvironment:test " ) return 1 ;
if ( zone . id == = " deploymentEnvironment:prod " ) return 2 ;
return 10 + hashString ( zone . id ) % 20 ;
} ;
const zoneLabelRank = ( zone , index ) = > {
if ( zone . id == = " deploymentEnvironment:test " ) return 0 ;
if ( zone . id == = " deploymentEnvironment:prod " ) return 1 ;
if ( zone . id == = " deploymentEnvironment:dev-tegwick " ) return 2 ;
return index % 4 ;
} ;
const zoneLabelTop = ( zone , bounds , index ) = > {
const rank = zone . field == = " deploymentEnvironment " ? zoneLabelRank ( zone , index ) : index % 4 ;
return Math . min ( 8 + rank * 28 , Math . max ( 8 , Math . round ( bounds . height - 32 ) ) ) ;
} ;
const zoneForData = ( data , grouping = activeZoneGrouping ) = > {
if ( grouping == = " deploymentEnvironment " ) {
const value = String ( data . deploymentEnvironment | | " " ) . trim ( ) ;
if ( ! value ) return null ;
const normalized = value . toLowerCase ( ) ;
if ( normalized == = " all " ) return null ;
const label = normalized == = " dev "
? " dev-tegwick "
: normalized == = " test " | | normalized == = " staging "
? " test "
: normalized == = " prod "
? normalized
: value ;
return {
field : " deploymentEnvironment " ,
id : ` deploymentEnvironment : $ { label } ` ,
label ,
value ,
} ;
}
if ( grouping == = " accessZone " ) {
const value = String ( data . accessZone | | " " ) . trim ( ) ;
if ( ! value ) return null ;
return {
field : " accessZone " ,
id : ` accessZone : $ { value } ` ,
label : value ,
value ,
} ;
}
return null ;
} ;
const renderedNodeBox = ( node ) = > {
try {
const box = node . renderedBoundingBox ( { includeLabels : true , includeOverlays : false } ) ;
if ( [ box . x1 , box . y1 , box . x2 , box . y2 ] . every ( Number . isFinite ) ) return box ;
} catch {
/ / Fall back to the rendered center point below .
}
const position = node . renderedPosition ( ) ;
const radius = ( Number ( node . data ( " visualSize " ) ) | | 44 ) / 2 ;
return {
x1 : position . x - radius ,
y1 : position . y - radius ,
x2 : position . x + radius ,
y2 : position . y + radius ,
} ;
} ;
const zoneBoundsForNodes = ( nodes ) = > {
if ( ! nodes . length ) return null ;
const bounds = nodes . reduce ( ( box , node ) = > {
const nodeBox = renderedNodeBox ( node ) ;
return {
x1 : Math . min ( box . x1 , nodeBox . x1 ) ,
y1 : Math . min ( box . y1 , nodeBox . y1 ) ,
x2 : Math . max ( box . x2 , nodeBox . x2 ) ,
y2 : Math . max ( box . y2 , nodeBox . y2 ) ,
} ;
} , { x1 : Infinity , y1 : Infinity , x2 : - Infinity , y2 : - Infinity } ) ;
if ( ! [ bounds . x1 , bounds . y1 , bounds . x2 , bounds . y2 ] . every ( Number . isFinite ) ) return null ;
const padding = 34 ;
let left = bounds . x1 - padding ;
let top = bounds . y1 - padding ;
let width = bounds . x2 - bounds . x1 + padding * 2 ;
let height = bounds . y2 - bounds . y1 + padding * 2 ;
const minWidth = 132 ;
const minHeight = 84 ;
if ( width < minWidth ) {
left - = ( minWidth - width ) / 2 ;
width = minWidth ;
}
if ( height < minHeight ) {
top - = ( minHeight - height ) / 2 ;
height = minHeight ;
}
return { left , top , width , height } ;
} ;
const collectZoneSummaries = ( ) = > {
const groups = new Map ( ) ;
if ( ! cy ) return groups ;
cy . elements ( ) . filter ( ( element ) = > element . style ( " display " ) != = " none " ) . forEach ( ( element ) = > {
const zone = zoneForData ( element . data ( ) ) ;
if ( ! zone ) return ;
if ( ! groups . has ( zone . id ) ) {
groups . set ( zone . id , { . . . zone , nodes : [ ] , elements : [ ] } ) ;
}
const group = groups . get ( zone . id ) ;
group . elements . push ( element ) ;
if ( element . isNode ( ) ) group . nodes . push ( element ) ;
} ) ;
return groups ;
} ;
const zoneCountsForField = ( elements , field ) = > {
if ( field != = " deploymentEnvironment " & & field != = " accessZone " ) return groupCounts ( elements , field ) ;
const groups = new Map ( ) ;
elements . forEach ( ( element ) = > {
const zone = zoneForData ( element . data ( ) , field ) ;
if ( ! zone ) return ;
groups . set ( zone . label , ( groups . get ( zone . label ) | | 0 ) + 1 ) ;
} ) ;
return Array . from ( groups . entries ( ) )
. sort ( ( left , right ) = > right [ 1 ] - left [ 1 ] | | left [ 0 ] . localeCompare ( right [ 0 ] ) ) ;
} ;
const renderZoneDetails = ( zone ) = > {
selected = null ;
selectedZoneId = zone . id ;
const visibleElements = zone . elements | | [ ] ;
const visibleNodes = zone . nodes | | [ ] ;
detailTitle . textContent = zone . label ;
detailSummary . textContent = ` $ { visibleNodes . length } visible node $ { visibleNodes . length == = 1 ? " " : " s " } in $ { ruleAttributeLabels [ zone . field ] | | humanize ( zone . field ) } ` ;
detailPills . innerHTML = [ zone . field , zone . value , activeZoneGrouping ]
. map ( ( value ) = > value ? ` < span class = " pill " > $ { escapeHtml ( value ) } < / span > ` : " " )
. join ( " " ) ;
const valuesFor = ( field ) = > groupCounts ( visibleElements , field )
. map ( ( [ value , count ] ) = > ` $ { value } ( $ { count } ) ` )
. join ( " , " ) ;
const warnings = visibleElements
. flatMap ( ( element ) = > zoneWarningsForData ( element . data ( ) ) . map ( ( warning ) = >
` $ { elementLabel ( element ) } : $ { warning } `
) ) ;
const rows = [
[ " visible nodes " , String ( visibleNodes . length ) ] ,
[ " deployment environments " , valuesFor ( " deploymentEnvironment " ) ] ,
[ " deployment scenarios " , valuesFor ( " deploymentScenario " ) ] ,
[ " access zones " , valuesFor ( " accessZone " ) ] ,
[ " routing authorities " , valuesFor ( " routingAuthority " ) ] ,
[ " policy authorities " , valuesFor ( " policyAuthority " ) ] ,
. . . warnings . slice ( 0 , 8 ) . map ( ( warning ) = > [ " warning " , warning ] ) ,
] ;
if ( warnings . length > 8 ) rows . push ( [ " warning " , ` $ { warnings . length - 8 } additional route warnings ` ] ) ;
detailList . innerHTML = rows
. filter ( ( [ , value ] ) = > value )
. map ( ( [ key , value ] ) = > ` < li class = " $ { key === " warning " ? " orientation - warning " : " " } " > < strong > $ { escapeHtml ( key ) } < / strong > $ { escapeHtml ( value ) } < / li > ` )
. join ( " " ) ;
const contextIds = new Set ( visibleNodes . map ( ( node ) = > node . id ( ) ) ) ;
visibleElements . filter ( ( element ) = > element . isEdge ( ) ) . forEach ( ( edge ) = > addContextEdge ( contextIds , edge ) ) ;
orientationContext = {
ids : Array . from ( contextIds ) ,
profileName : ` Zone : $ { zone . label } ` ,
} ;
orientationTitle . textContent = ` Zone context ( $ { orientationContext . ids . length } items ) ` ;
orientationList . innerHTML = [
{ label : " grouping " , value : ruleAttributeLabels [ zone . field ] | | humanize ( zone . field ) , state : " good " } ,
{ label : " zone " , value : zone . label } ,
{ label : " warnings " , value : warnings . length ? String ( warnings . length ) : " none " , state : warnings . length ? " warning " : " good " } ,
] . map ( ( row ) = > `
< li class = " orientation-item $ { orientationStateClass(row.state)} " >
< span class = " orientation-label " > $ { escapeHtml ( row . label ) } < / span >
< span class = " orientation-value " > $ { escapeHtml ( row . value ) } < / span >
< / li >
` ) . join ( " " ) ;
orientationActions . innerHTML = `
< button type = " button " data - orientation - action = " focus " > Focus Context < / button >
< button type = " button " data - orientation - action = " highlight " > Highlight Context < / button >
< button type = " button " data - orientation - action = " hide-other " > Hide Other < / button >
< button type = " button " data - orientation - action = " remove-other " > Remove Other < / button >
< button type = " button " data - orientation - action = " name-view " > Name View < / button >
` ;
updateLabelVisibility ( ) ;
updateSelectionAnchor ( ) ;
} ;
const renderZoneOverlay = ( ) = > {
if ( ! zoneOverlay | | ! cy ) return ;
const enabled = zoneBoundaryToggle ? zoneBoundaryToggle . checked : true ;
if ( ! enabled ) {
zoneSummaries = new Map ( ) ;
selectedZoneId = " " ;
zoneOverlay . innerHTML = " " ;
return ;
}
zoneSummaries = collectZoneSummaries ( ) ;
const boundaries = Array . from ( zoneSummaries . values ( ) )
. filter ( ( zone ) = > zone . nodes . length > 0 )
. sort ( ( left , right ) = > zoneRank ( left ) - zoneRank ( right ) | | left . label . localeCompare ( right . label ) )
. map ( ( zone , index ) = > {
const bounds = zoneBoundsForNodes ( zone . nodes ) ;
if ( ! bounds ) return " " ;
const style = zoneStyle ( zone ) ;
const selectedClass = selectedZoneId == = zone . id ? " selected " : " " ;
const labelTop = zoneLabelTop ( zone , bounds , index ) ;
return `
< div class = " zone-boundary$ {selectedClass} " data - zone - id = " $ { escapeHtml(zone.id)} " style = " --zone-color:$ {style.color} ;--zone-fill:$ {style.fill} ;left:$ { Math.round(bounds.left)}px;top:$ { Math.round(bounds.top)}px;width:$ { Math.round(bounds.width)}px;height:$ { Math.round(bounds.height)}px " >
< button type = " button " class = " zone-label " data - zone - id = " $ { escapeHtml(zone.id)} " style = " --zone-color:$ {style.color} ;top:$ {labelTop} px " title = " $ { escapeHtml(zone.label)} " > $ { escapeHtml ( zone . label ) } < / button >
< / div >
` ;
} ) ;
zoneOverlay . innerHTML = boundaries . join ( " " ) ;
if ( selectedZoneId & & zoneSummaries . has ( selectedZoneId ) ) {
renderZoneDetails ( zoneSummaries . get ( selectedZoneId ) ) ;
} else if ( selectedZoneId ) {
selectedZoneId = " " ;
}
} ;
const scheduleZoneOverlayUpdate = ( ) = > {
if ( ! zoneOverlay | | zoneOverlayFrame ) return ;
zoneOverlayFrame = window . requestAnimationFrame ( ( ) = > {
zoneOverlayFrame = 0 ;
renderZoneOverlay ( ) ;
} ) ;
} ;
2026-05-24 15:55:05 +02:00
const groupCounts = ( elements , field ) = > {
const counts = new Map ( ) ;
elements . forEach ( ( element ) = > {
const value = element . data ( field ) ;
if ( hasValue ( value ) ) counts . set ( String ( value ) , ( counts . get ( String ( value ) ) | | 0 ) + 1 ) ;
} ) ;
return Array . from ( counts . entries ( ) )
. sort ( ( left , right ) = > right [ 1 ] - left [ 1 ] | | left [ 0 ] . localeCompare ( right [ 0 ] ) ) ;
} ;
const renderMapOverview = ( ) = > {
const visibleElements = cy
? cy . elements ( ) . filter ( ( element ) = > element . style ( " display " ) != = " none " ) . toArray ( )
: [ ] ;
const visibleNodes = visibleElements . filter ( ( element ) = > element . isNode ( ) ) . length ;
const visibleEdges = visibleElements . filter ( ( element ) = > element . isEdge ( ) ) . length ;
const fields = zoneModeFields [ activeMode ] | | [ ] ;
const title = zoneModeLabels [ activeMode ] | | " Full " ;
detailTitle . textContent = activeMode == = " full " ? " Fabric Map " : ` $ { title } View ` ;
detailSummary . textContent = ` $ { visibleNodes } nodes / $ { visibleEdges } edges visible ` ;
detailPills . innerHTML = activeMode == = " full " ? " " : ` < span class = " pill " > $ { escapeHtml ( activeMode ) } < / span > ` ;
const rows = [ { label : " visible " , value : ` $ { visibleNodes } nodes / $ { visibleEdges } edges ` } ] ;
fields . forEach ( ( field ) = > {
2026-05-24 17:02:12 +02:00
const counts = zoneCountsForField ( visibleElements , field ) ;
2026-05-24 15:55:05 +02:00
rows . push ( {
label : ruleAttributeLabels [ field ] | | humanize ( field ) ,
value : counts . length
? counts . slice ( 0 , 8 ) . map ( ( [ value , count ] ) = > ` $ { value } ( $ { count } ) ` ) . join ( " , " )
: " no annotated visible entities " ,
state : counts . length ? " " : " warning " ,
} ) ;
} ) ;
const warnings = visibleElements
. flatMap ( ( element ) = > zoneWarningsForData ( element . data ( ) ) . map ( ( warning ) = >
` $ { elementLabel ( element ) } : $ { warning } `
) ) ;
warnings . slice ( 0 , 6 ) . forEach ( ( warning ) = > rows . push ( { label : " warning " , value : warning , state : " warning " } ) ) ;
if ( warnings . length > 6 ) {
rows . push ( { label : " warning " , value : ` $ { warnings . length - 6 } additional route warnings ` , state : " warning " } ) ;
}
detailList . innerHTML = rows
. filter ( ( row ) = > row . value )
. map ( ( row ) = > ` < li class = " $ { orientationStateClass(row.state)} " > < strong > $ { escapeHtml ( row . label ) } < / strong > $ { escapeHtml ( row . value ) } < / li > ` )
. join ( " " ) ;
} ;
2026-05-19 01:46:23 +02:00
const collectionArray = ( collection ) = > Array . from ( collection | | [ ] ) ;
const orientationStateClass = ( state ) = >
state == = " warning " ? " orientation-warning " : state == = " good " ? " orientation-good " : " " ;
2026-05-19 00:16:57 +02:00
const renderedElementPosition = ( element ) = > {
if ( ! element | | ! cy ) return null ;
if ( element . isNode & & element . isNode ( ) ) return element . renderedPosition ( ) ;
if ( element . isEdge & & element . isEdge ( ) ) {
if ( typeof element . renderedMidpoint == = " function " ) {
const midpoint = element . renderedMidpoint ( ) ;
if ( isFinitePoint ( midpoint ) ) return midpoint ;
}
const source = cy . getElementById ( element . data ( " source " ) ) ;
const target = cy . getElementById ( element . data ( " target " ) ) ;
if ( source . length > 0 & & target . length > 0 ) {
const sourcePosition = source . renderedPosition ( ) ;
const targetPosition = target . renderedPosition ( ) ;
if ( isFinitePoint ( sourcePosition ) & & isFinitePoint ( targetPosition ) ) {
return {
x : ( sourcePosition . x + targetPosition . x ) / 2 ,
y : ( sourcePosition . y + targetPosition . y ) / 2 ,
} ;
}
}
}
return null ;
} ;
const updateSelectionAnchor = ( ) = > {
if ( ! selected | | ! selectionAnchor ) {
selectedAnchor = null ;
selectionAnchor . style . display = " none " ;
return ;
}
const position = renderedElementPosition ( selected ) ;
if ( isFinitePoint ( position ) ) {
selectedAnchor = { x : position . x , y : position . y , label : elementLabel ( selected ) } ;
}
if ( ! selectedAnchor ) {
selectionAnchor . style . display = " none " ;
return ;
}
selectionAnchor . style . left = ` $ { selectedAnchor . x } px ` ;
selectionAnchor . style . top = ` $ { selectedAnchor . y } px ` ;
selectionAnchorLabel . textContent = selectedAnchor . label ;
selectionAnchor . style . display = " block " ;
} ;
2026-05-19 00:27:09 +02:00
const isKeyLabelNode = ( node ) = > {
const layer = node . data ( " layer " ) ;
return (
[ " repository " , " server " , " deployment " , " service " ] . includes ( layer ) | |
node . data ( " unresolved " ) == = true | |
node . data ( " lifecycle " ) == = " registered-only "
) ;
} ;
const updateLabelVisibility = ( ) = > {
if ( ! cy ) return ;
const visibleNodes = cy . nodes ( ) . filter ( ( node ) = > node . style ( " display " ) != = " none " ) ;
const mode = activeLabelMode == = " auto " & & visibleNodes . length > 70 ? " key " : activeLabelMode ;
cy . nodes ( ) . forEach ( ( node ) = > {
let showLabel = mode == = " all " | | mode == = " auto " ;
if ( mode == = " key " ) showLabel = isKeyLabelNode ( node ) ;
if ( mode == = " none " ) showLabel = false ;
if ( selected & & node . id ( ) == = selected . id ( ) ) showLabel = false ;
node . toggleClass ( " label-hidden " , ! showLabel ) ;
} ) ;
} ;
2026-05-18 20:14:07 +02:00
const currentViewState = ( ) = > ( {
search : searchInput . value ,
mode : modeSelect . value | | " full " ,
layout : layoutSelect . value | | " cose " ,
2026-05-19 00:27:09 +02:00
labelMode : labelSelect . value | | " auto " ,
2026-05-18 23:58:58 +02:00
nodeTypes : Array . from ( selectedNodeTypes ( ) ) ,
edgeTypes : Array . from ( selectedEdgeTypes ( ) ) ,
2026-05-18 20:14:07 +02:00
review : reviewFilter . value ,
unresolved : unresolvedFilter . value ,
2026-05-24 17:02:12 +02:00
zoneBoundaries : zoneBoundaryToggle ? zoneBoundaryToggle . checked : true ,
zoneGrouping : zoneGroupSelect ? zoneGroupSelect . value | | " deploymentEnvironment " : " deploymentEnvironment " ,
2026-05-19 01:16:18 +02:00
rules : filterRules . map ( ( rule ) = > ( { . . . rule } ) ) ,
2026-05-18 20:14:07 +02:00
manualOverrides : { . . . manualOverrides } ,
} ) ;
const encodeStateBlob = ( state ) = > {
const bytes = new TextEncoder ( ) . encode ( JSON . stringify ( state ) ) ;
let binary = " " ;
bytes . forEach ( ( byte ) = > { binary + = String . fromCharCode ( byte ) ; } ) ;
return btoa ( binary ) ;
} ;
const decodeStateBlob = ( blob ) = > {
const bytes = Uint8Array . from ( atob ( blob ) , ( char ) = > char . charCodeAt ( 0 ) ) ;
return JSON . parse ( new TextDecoder ( ) . decode ( bytes ) ) ;
} ;
const readUrlState = ( ) = > {
const params = new URLSearchParams ( window . location . search ) ;
const state = { } ;
if ( params . has ( " search " ) ) state . search = params . get ( " search " ) | | " " ;
if ( params . has ( " mode " ) ) state . mode = params . get ( " mode " ) | | " " ;
if ( params . has ( " layout " ) ) state . layout = params . get ( " layout " ) | | " " ;
2026-05-19 00:27:09 +02:00
if ( params . has ( " labelMode " ) ) state . labelMode = params . get ( " labelMode " ) | | " " ;
2026-05-18 23:58:58 +02:00
if ( params . has ( " nodeTypes " ) ) state . nodeTypes = parseListParam ( params . get ( " nodeTypes " ) ) ;
if ( params . has ( " edgeTypes " ) ) state . edgeTypes = parseListParam ( params . get ( " edgeTypes " ) ) ;
if ( params . has ( " layer " ) ) state . nodeTypes = parseListParam ( params . get ( " layer " ) ) ;
2026-05-18 20:14:07 +02:00
if ( params . has ( " review " ) ) state . review = params . get ( " review " ) | | " " ;
if ( params . has ( " unresolved " ) ) state . unresolved = params . get ( " unresolved " ) | | " " ;
2026-05-24 17:02:12 +02:00
if ( params . has ( " zoneBoundaries " ) ) state . zoneBoundaries = params . get ( " zoneBoundaries " ) != = " 0 " ;
if ( params . has ( " zoneGrouping " ) ) state . zoneGrouping = params . get ( " zoneGrouping " ) | | " " ;
2026-05-18 20:14:07 +02:00
if ( params . has ( " profile " ) ) state . profile = params . get ( " profile " ) | | " " ;
if ( params . has ( " state " ) ) {
try {
Object . assign ( state , decodeStateBlob ( params . get ( " state " ) | | " " ) ) ;
} catch ( error ) {
profileSummary . textContent = ` Could not load copied map state : $ { error . message } ` ;
}
}
return state ;
} ;
const viewUrl = ( includeStateBlob = false ) = > {
const params = new URLSearchParams ( ) ;
const state = currentViewState ( ) ;
if ( state . search ) params . set ( " search " , state . search ) ;
if ( state . mode & & state . mode != = " full " ) params . set ( " mode " , state . mode ) ;
if ( state . layout & & state . layout != = " cose " ) params . set ( " layout " , state . layout ) ;
2026-05-19 00:27:09 +02:00
if ( state . labelMode & & state . labelMode != = " auto " ) params . set ( " labelMode " , state . labelMode ) ;
2026-05-18 23:58:58 +02:00
if ( state . nodeTypes . length != = allNodeTypes . length ) params . set ( " nodeTypes " , state . nodeTypes . join ( " , " ) ) ;
if ( state . edgeTypes . length != = allEdgeTypes . length ) params . set ( " edgeTypes " , state . edgeTypes . join ( " , " ) ) ;
2026-05-18 20:14:07 +02:00
if ( state . review ) params . set ( " review " , state . review ) ;
if ( state . unresolved ) params . set ( " unresolved " , state . unresolved ) ;
2026-05-24 17:02:12 +02:00
if ( state . zoneBoundaries == = false ) params . set ( " zoneBoundaries " , " 0 " ) ;
if ( state . zoneGrouping & & state . zoneGrouping != = " deploymentEnvironment " ) params . set ( " zoneGrouping " , state . zoneGrouping ) ;
2026-05-18 20:14:07 +02:00
if ( currentProfileId ) params . set ( " profile " , currentProfileId ) ;
2026-05-19 01:16:18 +02:00
if ( includeStateBlob | | hasManualOverrides ( ) | | filterRules . length > 0 ) {
params . set ( " state " , encodeStateBlob ( state ) ) ;
}
2026-05-18 20:14:07 +02:00
const query = params . toString ( ) ;
return ` $ { window . location . pathname } $ { query ? ` ? $ { query } ` : " " } $ { window . location . hash } ` ;
} ;
const updateUrlState = ( ) = > {
if ( ! window . history | | ! window . history . replaceState ) return ;
window . history . replaceState ( { } , " " , viewUrl ( false ) ) ;
} ;
const optionExists = ( select , value ) = >
Array . from ( select . options ) . some ( ( option ) = > option . value == = value ) ;
const applyViewState = ( state , options = { } ) = > {
if ( " search " in state ) searchInput . value = state . search | | " " ;
if ( " mode " in state ) modeSelect . value = optionExists ( modeSelect , state . mode ) ? state . mode : " full " ;
if ( " layout " in state ) layoutSelect . value = optionExists ( layoutSelect , state . layout ) ? state . layout : " cose " ;
2026-05-19 00:27:09 +02:00
if ( " labelMode " in state ) labelSelect . value = optionExists ( labelSelect , state . labelMode ) ? state . labelMode : " auto " ;
2026-05-18 23:58:58 +02:00
if ( " nodeTypes " in state ) setCheckedValues ( nodeTypeFilter , ( state . nodeTypes | | [ ] ) . filter ( ( value ) = > allNodeTypes . includes ( value ) ) ) ;
if ( " layer " in state ) setCheckedValues ( nodeTypeFilter , allNodeTypes . includes ( state . layer ) ? [ state . layer ] : null ) ;
if ( " edgeTypes " in state ) setCheckedValues ( edgeTypeFilter , ( state . edgeTypes | | [ ] ) . filter ( ( value ) = > allEdgeTypes . includes ( value ) ) ) ;
2026-05-18 20:14:07 +02:00
if ( " review " in state ) reviewFilter . value = optionExists ( reviewFilter , state . review ) ? state . review : " " ;
if ( " unresolved " in state ) unresolvedFilter . value = optionExists ( unresolvedFilter , state . unresolved ) ? state . unresolved : " " ;
2026-05-24 17:02:12 +02:00
if ( " zoneBoundaries " in state & & zoneBoundaryToggle ) zoneBoundaryToggle . checked = state . zoneBoundaries != = false ;
if ( " zoneGrouping " in state & & zoneGroupSelect ) {
zoneGroupSelect . value = optionExists ( zoneGroupSelect , state . zoneGrouping ) ? state . zoneGrouping : " deploymentEnvironment " ;
}
2026-05-18 20:14:07 +02:00
if ( " manualOverrides " in state & & state . manualOverrides & & typeof state . manualOverrides == = " object " ) {
manualOverrides = { . . . state . manualOverrides } ;
}
2026-05-19 01:16:18 +02:00
if ( " rules " in state ) {
filterRules = normalizeRules ( state . rules ) ;
editingRuleId = " " ;
refreshRuleBuilder ( ) ;
renderRules ( ) ;
}
2026-05-18 20:14:07 +02:00
activeMode = modeSelect . value | | " full " ;
2026-05-19 00:27:09 +02:00
activeLabelMode = labelSelect . value | | " auto " ;
2026-05-24 17:02:12 +02:00
activeZoneGrouping = zoneGroupSelect ? zoneGroupSelect . value | | " deploymentEnvironment " : " deploymentEnvironment " ;
selectedZoneId = " " ;
2026-05-18 20:14:07 +02:00
focusSet = null ;
2026-05-18 23:58:58 +02:00
syncFilterSummaries ( ) ;
2026-05-18 20:14:07 +02:00
applyFilters ( ) ;
if ( ! options . skipLayout ) runLayout ( ) ;
if ( ! options . skipUrl ) updateUrlState ( ) ;
} ;
const loadProfiles = ( ) = > {
if ( ! supportsLocalProfiles ( ) ) return [ ] ;
try {
const parsed = JSON . parse ( window . localStorage . getItem ( profileStorageKey ) | | " [] " ) ;
return Array . isArray ( parsed )
? parsed . filter ( ( profile ) = > profile & & typeof profile . id == = " string " )
: [ ] ;
} catch {
return [ ] ;
}
} ;
const persistProfiles = ( ) = > {
if ( ! supportsLocalProfiles ( ) ) return ;
window . localStorage . setItem ( profileStorageKey , JSON . stringify ( profiles ) ) ;
} ;
const selectedProfile = ( ) = > profiles . find ( ( profile ) = > profile . id == = currentProfileId ) | | null ;
2026-05-19 01:46:23 +02:00
const viewStateSummary = ( ) = > {
const parts = [ ] ;
if ( searchInput . value . trim ( ) ) parts . push ( " search " ) ;
if ( ( modeSelect . value | | " full " ) != = " full " ) parts . push ( modeSelect . options [ modeSelect . selectedIndex ] ? . textContent | | " mode " ) ;
if ( ( labelSelect . value | | " auto " ) != = " auto " ) parts . push ( ` $ { labelSelect . value } labels ` ) ;
if ( selectedNodeTypes ( ) . size != = allNodeTypes . length ) parts . push ( " node filter " ) ;
if ( selectedEdgeTypes ( ) . size != = allEdgeTypes . length ) parts . push ( " edge filter " ) ;
if ( reviewFilter . value ) parts . push ( ` $ { reviewFilter . value } review ` ) ;
if ( unresolvedFilter . value ) parts . push ( " unresolved only " ) ;
2026-05-24 17:02:12 +02:00
if ( zoneBoundaryToggle & & ! zoneBoundaryToggle . checked ) parts . push ( " zones off " ) ;
if ( zoneGroupSelect & & ( zoneGroupSelect . value | | " deploymentEnvironment " ) != = " deploymentEnvironment " ) {
parts . push ( " access zone boundaries " ) ;
}
2026-05-19 01:46:23 +02:00
if ( filterRules . length ) parts . push ( ` $ { filterRules . length } rule $ { filterRules . length == = 1 ? " " : " s " } ` ) ;
const overrideCount = Object . keys ( manualOverrides ) . length ;
if ( overrideCount ) parts . push ( ` $ { overrideCount } override $ { overrideCount == = 1 ? " " : " s " } ` ) ;
return parts . length ? parts . join ( " , " ) : " no filters " ;
} ;
2026-05-18 20:14:07 +02:00
const updateProfileSummary = ( message = " " ) = > {
if ( ! supportsLocalProfiles ( ) ) {
profileSummary . textContent = " Profile persistence unavailable for this host. " ;
return ;
}
const profile = selectedProfile ( ) ;
const prefix = profile ? ` Loaded " $ {profile.name} " . ` : " Unsaved exploration. " ;
2026-05-19 01:46:23 +02:00
profileSummary . textContent = message | | ` $ { prefix } Current state : $ { viewStateSummary ( ) } . $ { profiles . length } saved view $ { profiles . length == = 1 ? " " : " s " } . ` ;
2026-05-18 20:14:07 +02:00
} ;
const updateProfileControls = ( ) = > {
const enabled = supportsLocalProfiles ( ) ;
profileSelect . disabled = ! enabled ;
profileNameInput . disabled = ! enabled ;
document . querySelectorAll ( " [data-profile-action] " ) . forEach ( ( button ) = > {
const action = button . dataset . profileAction ;
button . disabled = ! enabled | | ( ( action == = " duplicate " | | action == = " delete " ) & & ! currentProfileId ) ;
} ) ;
} ;
const renderProfiles = ( ) = > {
profileSelect . innerHTML = " " ;
const unsaved = document . createElement ( " option " ) ;
unsaved . value = " " ;
unsaved . textContent = " Unsaved exploration " ;
profileSelect . appendChild ( unsaved ) ;
profiles
. slice ( )
. sort ( ( left , right ) = > String ( left . name ) . localeCompare ( String ( right . name ) ) )
. forEach ( ( profile ) = > {
const option = document . createElement ( " option " ) ;
option . value = profile . id ;
option . textContent = profile . name | | profile . id ;
profileSelect . appendChild ( option ) ;
} ) ;
if ( ! profiles . some ( ( profile ) = > profile . id == = currentProfileId ) ) currentProfileId = " " ;
profileSelect . value = currentProfileId ;
const profile = selectedProfile ( ) ;
profileNameInput . value = profile ? profile . name : " " ;
updateProfileControls ( ) ;
updateProfileSummary ( ) ;
} ;
const saveCurrentProfile = ( name , sourceProfile = null ) = > {
const now = new Date ( ) . toISOString ( ) ;
2026-05-19 01:46:23 +02:00
const profileName = name . trim ( ) | | sourceProfile ? . name | | orientationContext ? . profileName | | ` Fabric view $ { profiles . length + 1 } ` ;
2026-05-18 20:14:07 +02:00
if ( sourceProfile ) {
const profile = {
. . . sourceProfile ,
id : ` local - $ { Date . now ( ) . toString ( 36 ) } - $ { Math . random ( ) . toString ( 36 ) . slice ( 2 , 8 ) } ` ,
name : profileName ,
createdAt : now ,
updatedAt : now ,
} ;
profiles = [ . . . profiles , profile ] ;
currentProfileId = profile . id ;
} else if ( currentProfileId ) {
profiles = profiles . map ( ( profile ) = > profile . id == = currentProfileId
? { . . . profile , name : profileName , state : currentViewState ( ) , updatedAt : now }
: profile ) ;
} else {
const profile = {
id : ` local - $ { Date . now ( ) . toString ( 36 ) } - $ { Math . random ( ) . toString ( 36 ) . slice ( 2 , 8 ) } ` ,
name : profileName ,
state : currentViewState ( ) ,
createdAt : now ,
updatedAt : now ,
} ;
profiles = [ . . . profiles , profile ] ;
currentProfileId = profile . id ;
}
persistProfiles ( ) ;
renderProfiles ( ) ;
updateUrlState ( ) ;
} ;
2026-05-18 17:11:36 +02:00
const matchesFilters = ( element ) = > {
const data = element . data ( ) ;
2026-05-18 18:31:25 +02:00
const modeSet = visibleSetForMode ( ) ;
if ( modeSet & & ! modeSet . has ( element . id ( ) ) ) return false ;
2026-05-18 17:11:36 +02:00
const text = searchInput . value . trim ( ) . toLowerCase ( ) ;
if ( text & & ! elementText ( data ) . includes ( text ) ) return false ;
2026-05-18 23:58:58 +02:00
if ( element . isNode ( ) & & ! selectedNodeTypes ( ) . has ( data . layer ) ) return false ;
if ( element . isEdge ( ) & & ! selectedEdgeTypes ( ) . has ( data . edgeType ) ) return false ;
2026-05-18 17:11:36 +02:00
if ( reviewFilter . value & & data . reviewState != = reviewFilter . value ) return false ;
if ( unresolvedFilter . value == = " true " & & data . unresolved != = true ) return false ;
if ( focusSet & & ! focusSet . has ( element . id ( ) ) ) return false ;
return true ;
} ;
2026-05-18 18:31:25 +02:00
const visibleSetForMode = ( ) = > {
if ( ! cy ) return null ;
if ( focusSet ) return focusSet ;
if ( activeMode == = " full " ) return null ;
if ( activeMode == = " onboarding-gaps " ) {
return new Set ( cy . elements ( ) . filter ( ( element ) = >
element . data ( " lifecycle " ) == = " registered-only " | | element . data ( " unresolved " ) == = true
) . map ( ( element ) = > element . id ( ) ) ) ;
}
if ( activeMode == = " unresolved " ) {
return new Set ( cy . elements ( ) . filter ( ( element ) = > element . data ( " unresolved " ) == = true ) . map ( ( element ) = > element . id ( ) ) ) ;
}
2026-05-24 15:55:05 +02:00
const zoneFields = zoneModeFields [ activeMode ] | | [ ] ;
if ( zoneFields . length & & activeMode != = " by-fabric " ) {
return new Set ( cy . elements ( ) . filter ( ( element ) = >
zoneFields . some ( ( field ) = > hasValue ( element . data ( field ) ) )
) . map ( ( element ) = > element . id ( ) ) ) ;
}
2026-05-18 18:31:25 +02:00
if ( ! selected ) return null ;
if ( activeMode == = " selected-path " ) {
const collection = selected . union ( selected . predecessors ( ) ) . union ( selected . successors ( ) ) ;
return new Set ( collection . map ( ( element ) = > element . id ( ) ) ) ;
}
if ( activeMode == = " neighborhood " ) {
const collection = selected . union ( selected . neighborhood ( ) ) . union ( selected . neighborhood ( ) . neighborhood ( ) ) ;
return new Set ( collection . map ( ( element ) = > element . id ( ) ) ) ;
}
return null ;
} ;
2026-05-19 01:16:18 +02:00
const applyFilters = ( options = { } ) = > {
2026-05-18 17:11:36 +02:00
if ( ! cy ) return ;
2026-05-19 01:16:18 +02:00
const previousRemoved = options . redrawOnRemove ? ruleRemovalSignature ( ) : " " ;
2026-05-18 23:58:58 +02:00
syncFilterSummaries ( ) ;
2026-05-18 17:11:36 +02:00
const hiddenNodes = new Set ( ) ;
2026-05-19 01:16:18 +02:00
const removedNodes = new Set ( ) ;
2026-05-18 17:11:36 +02:00
cy . nodes ( ) . forEach ( ( node ) = > {
let state = matchesFilters ( node ) ? " show " : " hide " ;
2026-05-19 01:16:18 +02:00
const ruleAction = ruleActionFor ( node ) ;
if ( ruleAction ) state = ruleAction ;
if ( state != = " remove " ) {
const override = manualOverrideFor ( node ) ;
if ( override ) state = override ;
}
2026-05-18 17:11:36 +02:00
node . data ( " displayState " , state ) ;
node . toggleClass ( " display-blur " , state == = " blur " ) ;
2026-05-19 01:16:18 +02:00
node . toggleClass ( " rule-highlight " , state == = " highlight " ) ;
node . style ( " display " , state == = " hide " | | state == = " remove " ? " none " : " element " ) ;
if ( state == = " remove " ) removedNodes . add ( node . id ( ) ) ;
2026-05-18 17:11:36 +02:00
if ( state == = " hide " ) hiddenNodes . add ( node . id ( ) ) ;
} ) ;
cy . edges ( ) . forEach ( ( edge ) = > {
let state = matchesFilters ( edge ) ? " show " : " hide " ;
2026-05-19 01:16:18 +02:00
const ruleAction = ruleActionFor ( edge ) ;
if ( ruleAction ) state = ruleAction ;
if ( removedNodes . has ( edge . data ( " source " ) ) | | removedNodes . has ( edge . data ( " target " ) ) ) {
state = " remove " ;
} else if ( hiddenNodes . has ( edge . data ( " source " ) ) | | hiddenNodes . has ( edge . data ( " target " ) ) ) {
state = " hide " ;
}
if ( state != = " remove " ) {
const override = manualOverrideFor ( edge ) ;
if ( override ) state = override ;
}
2026-05-18 17:11:36 +02:00
edge . data ( " displayState " , state ) ;
edge . toggleClass ( " display-blur " , state == = " blur " ) ;
2026-05-19 01:16:18 +02:00
edge . toggleClass ( " rule-highlight " , state == = " highlight " ) ;
edge . style ( " display " , state == = " hide " | | state == = " remove " ? " none " : " element " ) ;
2026-05-18 17:11:36 +02:00
} ) ;
const visibleNodes = cy . nodes ( ) . filter ( ( node ) = > node . style ( " display " ) != = " none " ) . length ;
const visibleEdges = cy . edges ( ) . filter ( ( edge ) = > edge . style ( " display " ) != = " none " ) . length ;
2026-05-19 01:16:18 +02:00
const removed = cy . elements ( ) . filter ( ( element ) = > element . data ( " displayState " ) == = " remove " ) . length ;
const hidden = cy . elements ( ) . length - visibleNodes - visibleEdges - removed ;
2026-05-18 17:11:36 +02:00
counts . textContent = ` $ { visibleNodes } nodes / $ { visibleEdges } edges ` ;
2026-05-19 01:16:18 +02:00
hiddenSummary . textContent = removed ? ` Hidden $ { hidden } / Removed $ { removed } ` : ` Hidden $ { hidden } ` ;
2026-05-19 00:27:09 +02:00
updateLabelVisibility ( ) ;
2026-05-19 00:16:57 +02:00
updateSelectionAnchor ( ) ;
2026-05-24 17:02:12 +02:00
scheduleZoneOverlayUpdate ( ) ;
2026-05-24 15:55:05 +02:00
if ( ! selected ) renderMapOverview ( ) ;
2026-05-19 01:16:18 +02:00
if ( options . redrawOnRemove & & previousRemoved != = ruleRemovalSignature ( ) ) runLayout ( ) ;
2026-05-18 17:11:36 +02:00
} ;
const showDetails = ( element ) = > {
selected = element | | null ;
2026-05-24 17:02:12 +02:00
selectedZoneId = " " ;
scheduleZoneOverlayUpdate ( ) ;
2026-05-18 17:11:36 +02:00
if ( ! element ) {
2026-05-19 01:46:23 +02:00
orientationContext = null ;
2026-05-24 15:55:05 +02:00
renderMapOverview ( ) ;
2026-05-18 18:36:15 +02:00
orientationTitle . textContent = " Select a service, interface, dependency, or registered-only repo. " ;
orientationList . innerHTML = " " ;
2026-05-19 01:46:23 +02:00
orientationActions . innerHTML = " " ;
2026-05-19 00:27:09 +02:00
updateLabelVisibility ( ) ;
2026-05-19 00:16:57 +02:00
updateSelectionAnchor ( ) ;
2026-05-18 17:11:36 +02:00
return ;
}
const data = element . data ( ) ;
detailTitle . textContent = data . name | | data . label | | data . id ;
detailSummary . textContent = data . description | | data . id ;
2026-05-18 23:58:58 +02:00
const nodeType = data . layer ? nodeTypeLabels [ data . layer ] | | humanize ( data . layer ) : " " ;
2026-05-23 14:00:59 +02:00
detailPills . innerHTML = [ data . kind , nodeType , data . canonCategory | | data . canonicalType , data . evidenceState , data . repo , data . reviewState , data . displayState ]
2026-05-18 23:58:58 +02:00
. map ( ( value ) = > value ? ` < span class = " pill " > $ { escapeHtml ( value ) } < / span > ` : " " )
2026-05-18 17:11:36 +02:00
. join ( " " ) ;
const links = data . deepLinks | | { } ;
const refs = data . sourceReferences | | [ ] ;
const rows = [
[ " id " , data . id ] ,
[ " source " , data . source ] ,
[ " target " , data . target ] ,
[ " edge " , data . edgeType ] ,
2026-05-23 14:00:59 +02:00
[ " canonical " , data . canonicalType | | data . canonCategory ] ,
[ " evidence " , data . evidenceState ] ,
[ " mapping " , data . mappingFit ] ,
[ " display only " , data . displayOnly == = true ? " yes " : " " ] ,
2026-05-18 17:11:36 +02:00
[ " strength " , data . strength ] ,
2026-05-24 15:55:05 +02:00
[ " deployment environment " , data . deploymentEnvironment ] ,
[ " deployment scenario " , data . deploymentScenario ] ,
[ " routing authority " , data . routingAuthority ] ,
[ " access zone " , data . accessZone ] ,
[ " policy authority " , data . policyAuthority ] ,
[ " exposure " , data . exposureClass ] ,
[ " route " , routeLabel ( data ) ] ,
. . . zoneWarningsForData ( data ) . map ( ( warning ) = > [ " warning " , warning ] ) ,
2026-05-18 17:11:36 +02:00
. . . Object . entries ( links ) ,
. . . refs . map ( ( ref ) = > [ ref . label | | ref . kind | | " source " , ref . path | | ref . url | | ref . ref | | " " ] )
] ;
detailList . innerHTML = rows
. filter ( ( [ , value ] ) = > value )
. map ( ( [ key , value ] ) = > ` < li > < strong > $ { escapeHtml ( key ) } < / strong > $ { escapeHtml ( value ) } < / li > ` )
. join ( " " ) ;
2026-05-18 18:36:15 +02:00
renderOrientation ( element ) ;
2026-05-19 00:27:09 +02:00
updateLabelVisibility ( ) ;
2026-05-19 00:16:57 +02:00
updateSelectionAnchor ( ) ;
2026-05-18 18:36:15 +02:00
} ;
const renderOrientation = ( element ) = > {
const data = element . data ( ) ;
2026-05-19 01:46:23 +02:00
const contextIds = new Set ( [ element . id ( ) ] ) ;
2026-05-18 18:36:15 +02:00
const rows = [ ] ;
2026-05-19 01:46:23 +02:00
let title = " Graph context " ;
let profileName = ` Fabric context : $ { elementLabel ( element ) } ` ;
2026-05-18 18:36:15 +02:00
if ( data . kind == = " Repository " & & data . lifecycle == = " registered-only " ) {
2026-05-19 01:46:23 +02:00
title = " Onboarding gap " ;
profileName = ` Onboarding gap : $ { data . repo | | elementLabel ( element ) } ` ;
rows . push (
{ label : " repo " , value : data . repo | | data . id } ,
{ label : " status " , value : " registered without an accepted Fabric graph snapshot " , state : " warning " } ,
{ label : " next " , value : " add Fabric declarations or run registry sync once a snapshot exists " }
) ;
2026-05-18 18:36:15 +02:00
} else if ( data . kind == = " InterfaceDeclaration " ) {
2026-05-19 01:46:23 +02:00
title = " Interface consumers and impact " ;
profileName = ` Interface impact : $ { elementLabel ( element ) } ` ;
const attributes = data . attributes | | { } ;
if ( attributes . service_id ) {
addContextNode ( contextIds , attributes . service_id ) ;
rows . push ( { label : " provider service " , value : nodeName ( attributes . service_id ) , state : " good " } ) ;
}
edgeCollection ( " " , data . id , " available_via " ) . forEach ( ( edge ) = > {
addContextEdge ( contextIds , edge ) ;
const capability = edge . data ( " source " ) ;
edgeCollection ( " " , capability , " provides " ) . forEach ( ( providerEdge ) = > {
addContextEdge ( contextIds , providerEdge ) ;
rows . push ( {
label : " provider surface " ,
value : ` $ { nodeName ( providerEdge . data ( " source " ) ) } - > $ { nodeName ( capability ) } - > $ { elementLabel ( element ) } ` ,
path : true ,
state : " good " ,
} ) ;
} ) ;
} ) ;
let consumerCount = 0 ;
edgeCollection ( " " , data . id , " uses_interface " ) . forEach ( ( edge ) = > {
addContextEdge ( contextIds , edge ) ;
2026-05-18 18:36:15 +02:00
const dependency = edge . data ( " source " ) ;
2026-05-19 01:46:23 +02:00
const dependencyNode = nodeById ( dependency ) ;
edgeCollection ( " " , dependency , " consumes " ) . forEach ( ( consumerEdge ) = > {
consumerCount + = 1 ;
addContextEdge ( contextIds , consumerEdge ) ;
rows . push ( {
label : " consumer " ,
value : ` $ { nodeName ( consumerEdge . data ( " source " ) ) } - > $ { nodeName ( dependency ) } - > $ { elementLabel ( element ) } ` ,
path : true ,
state : dependencyNode ? . data ( " unresolved " ) == = true ? " warning " : " " ,
} ) ;
} ) ;
2026-05-18 18:36:15 +02:00
} ) ;
2026-05-19 01:46:23 +02:00
if ( consumerCount == = 0 ) rows . push ( { label : " consumer " , value : " no accepted consumers in current graph " } ) ;
2026-05-18 18:36:15 +02:00
} else if ( data . kind == = " ServiceDeclaration " ) {
2026-05-19 01:46:23 +02:00
title = " Service dependency chain " ;
profileName = ` Service map : $ { elementLabel ( element ) } ` ;
let dependencyCount = 0 ;
edgeCollection ( data . id , " " , " consumes " ) . forEach ( ( edge ) = > {
dependencyCount + = 1 ;
addContextEdge ( contextIds , edge ) ;
2026-05-18 18:36:15 +02:00
const dependency = edge . data ( " target " ) ;
const providerEdges = cy . edges ( ) . filter ( ( candidate ) = >
2026-05-19 01:46:23 +02:00
candidate . data ( " source " ) == = dependency & & (
String ( candidate . data ( " edgeType " ) ) . startsWith ( " binds: " ) | |
candidate . data ( " edgeType " ) == = " uses_interface "
)
2026-05-18 18:36:15 +02:00
) ;
if ( providerEdges . length == = 0 ) {
2026-05-19 01:46:23 +02:00
rows . push ( {
label : " requires " ,
value : ` $ { elementLabel ( element ) } - > $ { nodeName ( dependency ) } - > unresolved ` ,
path : true ,
state : " warning " ,
} ) ;
2026-05-18 18:36:15 +02:00
return ;
}
providerEdges . forEach ( ( providerEdge ) = > {
2026-05-19 01:46:23 +02:00
addContextEdge ( contextIds , providerEdge ) ;
2026-05-18 18:36:15 +02:00
const target = providerEdge . data ( " target " ) ;
2026-05-19 01:46:23 +02:00
rows . push ( {
label : " requires " ,
value : ` $ { elementLabel ( element ) } - > $ { nodeName ( dependency ) } - > $ { nodeName ( target ) } ( $ { providerEdge . data ( " edgeType " ) } ) ` ,
path : true ,
state : String ( providerEdge . data ( " edgeType " ) ) . includes ( " missing " ) ? " warning " : " good " ,
} ) ;
} ) ;
} ) ;
if ( dependencyCount == = 0 ) rows . push ( { label : " requires " , value : " no declared dependencies " } ) ;
edgeCollection ( data . id , " " , " deployed_as " ) . forEach ( ( edge ) = > {
addContextEdge ( contextIds , edge ) ;
const deployment = edge . data ( " target " ) ;
const runtimeEdges = edgeCollection ( deployment , " " , " runs_on " ) ;
if ( runtimeEdges . length == = 0 ) {
rows . push ( { label : " deployment " , value : ` $ { nodeName ( deployment ) } - > no server binding ` , path : true , state : " warning " } ) ;
return ;
}
runtimeEdges . forEach ( ( runtimeEdge ) = > {
addContextEdge ( contextIds , runtimeEdge ) ;
rows . push ( {
label : " runtime " ,
value : ` $ { nodeName ( deployment ) } - > $ { nodeName ( runtimeEdge . data ( " target " ) ) } ` ,
path : true ,
state : " good " ,
} ) ;
2026-05-18 18:36:15 +02:00
} ) ;
} ) ;
} else if ( data . kind == = " DependencyDeclaration " ) {
2026-05-19 01:46:23 +02:00
title = " Dependency binding " ;
profileName = ` Dependency binding : $ { elementLabel ( element ) } ` ;
edgeCollection ( " " , data . id , " consumes " ) . forEach ( ( edge ) = > {
addContextEdge ( contextIds , edge ) ;
rows . push ( {
label : " consumer " ,
value : ` $ { nodeName ( edge . data ( " source " ) ) } - > $ { elementLabel ( element ) } ` ,
path : true ,
} ) ;
} ) ;
2026-05-18 18:36:15 +02:00
cy . edges ( ) . filter ( ( edge ) = >
edge . data ( " source " ) == = data . id & & (
String ( edge . data ( " edgeType " ) ) . startsWith ( " binds: " ) | |
edge . data ( " edgeType " ) == = " uses_interface "
)
) . forEach ( ( edge ) = > {
2026-05-19 01:46:23 +02:00
addContextEdge ( contextIds , edge ) ;
2026-05-18 18:36:15 +02:00
const target = edge . data ( " target " ) ;
2026-05-19 01:46:23 +02:00
rows . push ( {
label : edge . data ( " edgeType " ) ,
value : ` $ { elementLabel ( element ) } - > $ { nodeName ( target ) } ` ,
path : true ,
state : String ( edge . data ( " edgeType " ) ) . includes ( " missing " ) ? " warning " : " good " ,
} ) ;
2026-05-18 18:36:15 +02:00
} ) ;
2026-05-19 01:46:23 +02:00
if ( rows . length == = 0 ) rows . push ( { label : " binding " , value : " no provider binding in current graph " , state : " warning " } ) ;
2026-05-18 18:36:15 +02:00
} else if ( data . kind == = " CapabilityDeclaration " ) {
2026-05-19 01:46:23 +02:00
title = " Provider surface " ;
profileName = ` Provider surface : $ { elementLabel ( element ) } ` ;
edgeCollection ( " " , data . id , " provides " ) . forEach ( ( edge ) = > {
addContextEdge ( contextIds , edge ) ;
rows . push ( { label : " service " , value : ` $ { nodeName ( edge . data ( " source " ) ) } - > $ { elementLabel ( element ) } ` , path : true , state : " good " } ) ;
} ) ;
edgeCollection ( data . id , " " , " available_via " ) . forEach ( ( edge ) = > {
addContextEdge ( contextIds , edge ) ;
rows . push ( { label : " interface " , value : ` $ { elementLabel ( element ) } - > $ { nodeName ( edge . data ( " target " ) ) } ` , path : true , state : " good " } ) ;
} ) ;
if ( rows . length == = 0 ) rows . push ( { label : " surface " , value : " no provider surface in current graph " } ) ;
2026-05-18 18:36:15 +02:00
} else {
2026-05-19 01:46:23 +02:00
const neighborhood = element . neighborhood ( ) ;
neighborhood . forEach ( ( item ) = > addContextElement ( contextIds , item ) ) ;
rows . push ( { label : " neighbors " , value : ` $ { neighborhood . nodes ( ) . length } connected nodes ` } ) ;
2026-05-18 18:36:15 +02:00
}
2026-05-19 01:46:23 +02:00
orientationContext = {
ids : Array . from ( contextIds ) ,
profileName ,
} ;
orientationTitle . textContent = ` $ { title } ( $ { orientationContext . ids . length } items ) ` ;
2026-05-18 18:36:15 +02:00
orientationList . innerHTML = rows
2026-05-19 01:46:23 +02:00
. map ( ( row ) = > `
< li class = " orientation-item $ { orientationStateClass(row.state)} " >
< span class = " orientation-label " > $ { escapeHtml ( row . label ) } < / span >
< span class = " orientation-value $ { row.path ? " orientation - path " : " " } " > $ { escapeHtml ( row . value ) } < / span >
< / li >
` )
2026-05-18 18:36:15 +02:00
. join ( " " ) ;
2026-05-19 01:46:23 +02:00
orientationActions . innerHTML = `
< button type = " button " data - orientation - action = " focus " > Focus Context < / button >
< button type = " button " data - orientation - action = " highlight " > Highlight Context < / button >
< button type = " button " data - orientation - action = " hide-other " > Hide Other < / button >
< button type = " button " data - orientation - action = " remove-other " > Remove Other < / button >
< button type = " button " data - orientation - action = " name-view " > Name View < / button >
` ;
} ;
const applyOrientationContext = ( action ) = > {
if ( ! cy | | ! orientationContext ) return ;
const contextIds = new Set ( orientationContext . ids ) ;
if ( action == = " focus " ) {
focusSet = contextIds ;
modeSelect . value = " neighborhood " ;
activeMode = " neighborhood " ;
} else if ( action == = " highlight " ) {
contextIds . forEach ( ( id ) = > { manualOverrides [ id ] = " highlight " ; } ) ;
} else if ( action == = " hide-other " | | action == = " remove-other " ) {
const otherState = action == = " remove-other " ? " remove " : " hide " ;
cy . elements ( ) . forEach ( ( item ) = > {
manualOverrides [ item . id ( ) ] = contextIds . has ( item . id ( ) ) ? " show " : otherState ;
} ) ;
} else if ( action == = " name-view " ) {
if ( supportsLocalProfiles ( ) ) {
profileNameInput . value = orientationContext . profileName ;
updateProfileSummary ( ` Prepared view name " $ {orientationContext.profileName} " . ` ) ;
}
return ;
}
currentProfileId = " " ;
profileSelect . value = " " ;
applyFilters ( { redrawOnRemove : action == = " remove-other " } ) ;
updateProfileControls ( ) ;
updateProfileSummary ( ) ;
updateUrlState ( ) ;
2026-05-18 17:11:36 +02:00
} ;
const applyFocus = ( ) = > {
if ( ! cy | | ! selected ) return ;
const collection = selected . union ( selected . neighborhood ( ) ) . union ( selected . neighborhood ( ) . neighborhood ( ) ) ;
focusSet = new Set ( collection . map ( ( item ) = > item . id ( ) ) ) ;
2026-05-18 18:31:25 +02:00
modeSelect . value = " neighborhood " ;
activeMode = " neighborhood " ;
2026-05-18 20:14:07 +02:00
currentProfileId = " " ;
profileSelect . value = " " ;
2026-05-18 17:11:36 +02:00
applyFilters ( ) ;
2026-05-18 20:14:07 +02:00
updateProfileControls ( ) ;
updateProfileSummary ( ) ;
updateUrlState ( ) ;
2026-05-18 17:11:36 +02:00
} ;
2026-05-18 22:39:12 +02:00
const edgeIdealLength = ( edge ) = > Number ( edge . data ( " layoutIdealLength " ) ) | | 110 ;
const edgeElasticity = ( edge ) = > Number ( edge . data ( " layoutElasticity " ) ) | | 80 ;
2026-05-18 18:31:25 +02:00
const runLayout = ( ) = > {
if ( ! cy ) return ;
cy . elements ( ) . stop ( ) ;
2026-05-19 01:16:18 +02:00
const layoutElements = cy . elements ( ) . filter ( ( element ) = > element . data ( " displayState " ) != = " remove " ) ;
2026-05-18 18:31:25 +02:00
const name = layoutSelect . value | | " cose " ;
const options = name == = " breadthfirst "
? { name , directed : true , padding : 48 , animate : false }
2026-05-18 22:39:12 +02:00
: name == = " cose "
? {
name ,
padding : 48 ,
animate : false ,
randomize : false ,
nodeOverlap : 12 ,
idealEdgeLength : edgeIdealLength ,
edgeElasticity ,
nodeRepulsion : 5000 ,
gravity : 1 ,
numIter : 1400 ,
}
: { name , padding : 48 , animate : false } ;
2026-05-19 01:16:18 +02:00
layoutElements . layout ( options ) . run ( ) ;
2026-05-19 00:16:57 +02:00
updateSelectionAnchor ( ) ;
2026-05-24 17:02:12 +02:00
scheduleZoneOverlayUpdate ( ) ;
2026-05-18 18:31:25 +02:00
} ;
2026-05-18 17:11:36 +02:00
const renderLegend = ( layers ) = > {
2026-05-19 00:27:09 +02:00
const nodeTypes = layers . map ( ( layer ) = >
2026-05-18 17:11:36 +02:00
` < span class = " pill " > < span style = " display:inline-block;width:10px;height:10px;border-radius:50 % ;background:$ { escapeHtml(layer.color || " #667085")}"></span>${escapeHtml(layer.label)}</span>`
2026-05-19 00:27:09 +02:00
) ;
const statusItems = [
' <span class= " pill " ><span style= " display:inline-block;width:12px;height:12px;border:2px dashed #b45309;border-radius:50 % " ></span>Unresolved</span> ' ,
' <span class= " pill " ><span style= " display:inline-block;width:12px;height:12px;border:2px dashed #be123c;border-radius:3px " ></span>Registered only</span> ' ,
' <span class= " pill " ><span style= " display:inline-block;width:18px;border-top:2px dotted #98a2b3 " ></span>Weak edge</span> ' ,
] ;
legend . innerHTML = [ . . . nodeTypes , . . . statusItems ] . join ( " " ) ;
2026-05-18 17:11:36 +02:00
} ;
const boot = async ( ) = > {
const [ manifest , payload ] = await Promise . all ( [
fetch ( manifestUrl ) . then ( ( response ) = > response . json ( ) ) ,
fetch ( graphUrl ) . then ( ( response ) = > response . json ( ) )
] ) ;
2026-05-18 18:31:25 +02:00
( manifest . modes | | [ { id : " full " , label : " Full " } ] ) . forEach ( ( mode ) = > {
const option = document . createElement ( " option " ) ;
option . value = mode . id ;
option . textContent = mode . label ;
modeSelect . appendChild ( option ) ;
} ) ;
2026-05-18 20:14:07 +02:00
profilePersistence = manifest . profile_persistence | | " none " ;
2026-05-18 17:11:36 +02:00
layerColors = Object . fromEntries ( ( manifest . layers | | [ ] ) . map ( ( layer ) = > [ layer . id , layer . color ] ) ) ;
2026-05-18 23:58:58 +02:00
nodeTypeLabels = Object . fromEntries ( ( manifest . layers | | [ ] ) . map ( ( layer ) = > [ layer . id , layer . label ] ) ) ;
allNodeTypes = ( manifest . layers | | [ ] ) . map ( ( layer ) = > layer . id ) ;
allEdgeTypes = Array . from ( new Set ( ( payload . elements | | [ ] )
. map ( ( element ) = > element . data | | { } )
. filter ( ( data ) = > data . source & & data . target & & data . edgeType )
. map ( ( data ) = > data . edgeType )
) ) . sort ( ( left , right ) = > left . localeCompare ( right ) ) ;
renderChecklist ( nodeTypeFilter , allNodeTypes , nodeTypeLabels , " node-type " ) ;
renderChecklist ( edgeTypeFilter , allEdgeTypes , { } , " edge-type " ) ;
syncFilterSummaries ( ) ;
2026-05-18 17:11:36 +02:00
renderLegend ( manifest . layers | | [ ] ) ;
2026-05-18 20:14:07 +02:00
profiles = loadProfiles ( ) ;
renderProfiles ( ) ;
2026-05-18 17:11:36 +02:00
const elements = ( payload . elements | | [ ] ) . map ( ( element ) = > ( {
. . . element ,
data : { . . . element . data , color : layerColors [ element . data . layer ] | | " #667085 " }
} ) ) ;
2026-05-19 00:51:38 +02:00
if ( elements . length == = 0 ) {
canvas . innerHTML = " <p class= ' canvas-notice ' >No graph entities were returned by the registry.</p> " ;
counts . textContent = " 0 nodes / 0 edges " ;
hiddenSummary . textContent = " Hidden 0 " ;
return ;
}
canvas . innerHTML = " " ;
2026-05-18 17:11:36 +02:00
cy = cytoscape ( {
container : canvas ,
elements ,
2026-05-18 18:31:25 +02:00
layout : { name : layoutSelect . value | | " cose " , animate : false , randomize : false } ,
2026-05-18 17:11:36 +02:00
style : [
{ selector : " node " , style : {
" background-color " : " data(color) " ,
" border-color " : " #172033 " ,
" border-width " : 1 ,
" color " : " #172033 " ,
" font-size " : 11 ,
" height " : " data(visualSize) " ,
" label " : " data(label) " ,
" text-background-color " : " #ffffff " ,
" text-background-opacity " : .85 ,
" text-background-padding " : 2 ,
" text-wrap " : " wrap " ,
" text-max-width " : 130 ,
" width " : " data(visualSize) "
} } ,
2026-05-19 00:16:57 +02:00
{ selector : " node[layer = ' repository ' ] " , style : { " shape " : " round-rectangle " } } ,
{ selector : " node[layer = ' server ' ] " , style : { " shape " : " pentagon " } } ,
{ selector : " node[layer = ' deployment ' ] " , style : { " shape " : " diamond " } } ,
{ selector : " node[layer = ' service ' ] " , style : { " shape " : " hexagon " } } ,
{ selector : " node[layer = ' capability ' ] " , style : { " shape " : " vee " } } ,
{ selector : " node[layer = ' interface ' ] " , style : { " shape " : " rectangle " } } ,
{ selector : " node[layer = ' dependency ' ] " , style : { " shape " : " triangle " } } ,
{ selector : " node[layer = ' binding ' ] " , style : { " shape " : " rhomboid " } } ,
{ selector : " node[unresolved = true] " , style : { " border-color " : " #b45309 " , " border-style " : " dashed " , " border-width " : 3 } } ,
{ selector : " node[lifecycle = ' registered-only ' ] " , style : { " border-color " : " #be123c " , " border-style " : " dashed " , " border-width " : 3 } } ,
2026-05-18 17:11:36 +02:00
{ selector : " edge " , style : {
" curve-style " : " bezier " ,
" line-color " : " #98a2b3 " ,
" target-arrow-color " : " #98a2b3 " ,
" target-arrow-shape " : " triangle " ,
" width " : " data(edgeWidth) "
} } ,
{ selector : " edge[strength = ' strong ' ] " , style : { " line-color " : " #344054 " , " target-arrow-color " : " #344054 " } } ,
{ selector : " edge[strength = ' weak ' ] " , style : { " line-style " : " dotted " } } ,
2026-05-19 01:16:18 +02:00
{ selector : " node.rule-highlight " , style : {
" border-color " : " #2563eb " ,
" border-width " : 3 ,
" z-index " : 3
} } ,
{ selector : " edge.rule-highlight " , style : {
" line-color " : " #2563eb " ,
" target-arrow-color " : " #2563eb " ,
" width " : 5 ,
" z-index " : 3
} } ,
2026-05-19 00:16:57 +02:00
{ selector : " edge.hover " , style : {
" label " : " data(edgeType) " ,
" font-size " : 10 ,
" text-background-color " : " #ffffff " ,
" text-background-opacity " : .9 ,
" text-background-padding " : 2 ,
" z-index " : 2
} } ,
2026-05-18 17:11:36 +02:00
{ selector : " .display-blur " , style : { " opacity " : .24 , " label " : " " } } ,
2026-05-19 00:27:09 +02:00
{ selector : " .display-blur.hover " , style : { " opacity " : .78 , " label " : " data(label) " } } ,
{ selector : " .label-hidden " , style : { " label " : " " } } ,
2026-05-19 00:16:57 +02:00
{ selector : " :selected " , style : { " overlay-opacity " : 0 } }
2026-05-18 17:11:36 +02:00
]
} ) ;
2026-05-19 01:16:18 +02:00
refreshRuleBuilder ( ) ;
renderRules ( ) ;
2026-05-18 17:11:36 +02:00
cy . on ( " tap " , " node, edge " , ( event ) = > showDetails ( event . target ) ) ;
cy . on ( " tap " , ( event ) = > { if ( event . target == = cy ) showDetails ( null ) ; } ) ;
2026-05-24 17:02:12 +02:00
cy . on ( " pan zoom resize render layoutstop " , ( ) = > {
updateSelectionAnchor ( ) ;
scheduleZoneOverlayUpdate ( ) ;
} ) ;
cy . on ( " position " , " node " , ( ) = > {
updateSelectionAnchor ( ) ;
scheduleZoneOverlayUpdate ( ) ;
} ) ;
2026-05-18 17:11:36 +02:00
cy . on ( " mouseover " , " node, edge " , ( event ) = > {
event . target . addClass ( " hover " ) ;
const data = event . target . data ( ) ;
2026-05-18 23:58:58 +02:00
const nodeType = data . layer ? nodeTypeLabels [ data . layer ] | | humanize ( data . layer ) : " " ;
popup . innerHTML = ` < strong > $ { escapeHtml ( data . name | | data . label | | data . id ) } < / strong > < span class = " meta " > $ { escapeHtml ( data . kind ) } / $ { escapeHtml ( nodeType | | data . edgeType | | " " ) } < / span > ` ;
2026-05-18 17:11:36 +02:00
popup . style . left = ` $ { Math . min ( event . renderedPosition . x + 14 , canvas . clientWidth - 270 ) } px ` ;
popup . style . top = ` $ { Math . max ( 10 , event . renderedPosition . y + 14 ) } px ` ;
popup . style . display = " block " ;
} ) ;
cy . on ( " mouseout " , " node, edge " , ( event ) = > {
event . target . removeClass ( " hover " ) ;
popup . style . display = " none " ;
} ) ;
2026-05-18 20:14:07 +02:00
const urlState = readUrlState ( ) ;
if ( urlState . profile & & profiles . some ( ( profile ) = > profile . id == = urlState . profile ) ) {
currentProfileId = urlState . profile ;
profileSelect . value = currentProfileId ;
const profile = selectedProfile ( ) ;
profileNameInput . value = profile ? profile . name : " " ;
if ( profile ) applyViewState ( profile . state | | { } , { skipLayout : true , skipUrl : true } ) ;
}
applyViewState ( urlState , { skipUrl : true } ) ;
2026-05-18 18:31:25 +02:00
runLayout ( ) ;
2026-05-18 20:14:07 +02:00
updateUrlState ( ) ;
2026-05-18 17:11:36 +02:00
} ;
2026-05-18 23:58:58 +02:00
const handleFilterChange = ( ) = > {
focusSet = null ;
currentProfileId = " " ;
profileSelect . value = " " ;
applyFilters ( ) ;
updateProfileControls ( ) ;
updateProfileSummary ( ) ;
updateUrlState ( ) ;
} ;
[ searchInput , reviewFilter , unresolvedFilter ] . forEach ( ( control ) = > {
control . addEventListener ( " input " , handleFilterChange ) ;
2026-05-18 17:11:36 +02:00
} ) ;
2026-05-18 23:58:58 +02:00
nodeTypeFilter . addEventListener ( " change " , handleFilterChange ) ;
edgeTypeFilter . addEventListener ( " change " , handleFilterChange ) ;
2026-05-18 18:31:25 +02:00
modeSelect . addEventListener ( " input " , ( ) = > {
activeMode = modeSelect . value | | " full " ;
focusSet = null ;
2026-05-18 20:14:07 +02:00
currentProfileId = " " ;
profileSelect . value = " " ;
2026-05-18 18:31:25 +02:00
applyFilters ( ) ;
2026-05-18 20:14:07 +02:00
updateProfileControls ( ) ;
updateProfileSummary ( ) ;
updateUrlState ( ) ;
} ) ;
layoutSelect . addEventListener ( " input " , ( ) = > {
currentProfileId = " " ;
profileSelect . value = " " ;
runLayout ( ) ;
updateProfileControls ( ) ;
updateProfileSummary ( ) ;
updateUrlState ( ) ;
2026-05-18 18:31:25 +02:00
} ) ;
2026-05-19 00:27:09 +02:00
labelSelect . addEventListener ( " input " , ( ) = > {
activeLabelMode = labelSelect . value | | " auto " ;
currentProfileId = " " ;
profileSelect . value = " " ;
updateLabelVisibility ( ) ;
updateProfileControls ( ) ;
updateProfileSummary ( ) ;
updateUrlState ( ) ;
} ) ;
2026-05-24 17:02:12 +02:00
zoneBoundaryToggle . addEventListener ( " input " , ( ) = > {
selectedZoneId = " " ;
currentProfileId = " " ;
profileSelect . value = " " ;
renderZoneOverlay ( ) ;
if ( ! selected ) renderMapOverview ( ) ;
updateProfileControls ( ) ;
updateProfileSummary ( ) ;
updateUrlState ( ) ;
} ) ;
zoneGroupSelect . addEventListener ( " input " , ( ) = > {
activeZoneGrouping = zoneGroupSelect . value | | " deploymentEnvironment " ;
selectedZoneId = " " ;
currentProfileId = " " ;
profileSelect . value = " " ;
renderZoneOverlay ( ) ;
if ( ! selected ) renderMapOverview ( ) ;
updateProfileControls ( ) ;
updateProfileSummary ( ) ;
updateUrlState ( ) ;
} ) ;
zoneOverlay . addEventListener ( " click " , ( event ) = > {
const button = event . target . closest ( " [data-zone-id] " ) ;
if ( ! button ) return ;
const zone = zoneSummaries . get ( button . dataset . zoneId ) ;
if ( ! zone ) return ;
event . stopPropagation ( ) ;
selectedZoneId = zone . id ;
renderZoneOverlay ( ) ;
} ) ;
2026-05-19 01:16:18 +02:00
ruleTarget . addEventListener ( " input " , ( ) = > refreshRuleBuilder ( { target : ruleTarget . value } ) ) ;
ruleType . addEventListener ( " input " , ( ) = > refreshRuleBuilder ( { target : ruleTarget . value , type : ruleType . value } ) ) ;
ruleAttribute . addEventListener ( " input " , ( ) = > refreshRuleBuilder ( {
target : ruleTarget . value ,
type : ruleType . value ,
attribute : ruleAttribute . value ,
} ) ) ;
document . querySelector ( " [data-rule-action= ' save ' ] " ) . addEventListener ( " click " , ( ) = > {
const rule = {
id : editingRuleId | | ` rule - $ { Date . now ( ) . toString ( 36 ) } - $ { Math . random ( ) . toString ( 36 ) . slice ( 2 , 8 ) } ` ,
target : ruleTarget . value | | " node " ,
type : ruleType . value | | " " ,
attribute : ruleAttribute . value | | " any " ,
value : ruleAttribute . value == = " any " ? " " : ruleValue . value | | " " ,
action : ruleAction . value | | " highlight " ,
} ;
filterRules = editingRuleId
? filterRules . map ( ( existing ) = > existing . id == = editingRuleId ? rule : existing )
: [ . . . filterRules , rule ] ;
editingRuleId = " " ;
renderRules ( ) ;
applyFilters ( { redrawOnRemove : true } ) ;
currentProfileId = " " ;
profileSelect . value = " " ;
updateProfileControls ( ) ;
updateProfileSummary ( ) ;
updateUrlState ( ) ;
} ) ;
document . querySelector ( " [data-rule-action= ' cancel ' ] " ) . addEventListener ( " click " , ( ) = > {
editingRuleId = " " ;
refreshRuleBuilder ( ) ;
renderRules ( ) ;
} ) ;
document . querySelector ( " [data-rule-action= ' reset ' ] " ) . addEventListener ( " click " , ( ) = > {
filterRules = [ ] ;
editingRuleId = " " ;
refreshRuleBuilder ( ) ;
renderRules ( ) ;
applyFilters ( { redrawOnRemove : true } ) ;
currentProfileId = " " ;
profileSelect . value = " " ;
updateProfileControls ( ) ;
updateProfileSummary ( ) ;
updateUrlState ( ) ;
} ) ;
ruleList . addEventListener ( " click " , ( event ) = > {
const button = event . target . closest ( " [data-rule-list-action] " ) ;
if ( ! button ) return ;
const item = button . closest ( " [data-rule-id] " ) ;
const index = filterRules . findIndex ( ( rule ) = > rule . id == = item ? . dataset ? . ruleId ) ;
if ( index < 0 ) return ;
const action = button . dataset . ruleListAction ;
if ( action == = " edit " ) {
const rule = filterRules [ index ] ;
editingRuleId = rule . id ;
refreshRuleBuilder ( rule ) ;
renderRules ( ) ;
return ;
}
if ( action == = " delete " ) {
filterRules = filterRules . filter ( ( _ , ruleIndex ) = > ruleIndex != = index ) ;
} else if ( action == = " up " & & index > 0 ) {
[ filterRules [ index - 1 ] , filterRules [ index ] ] = [ filterRules [ index ] , filterRules [ index - 1 ] ] ;
} else if ( action == = " down " & & index < filterRules . length - 1 ) {
[ filterRules [ index + 1 ] , filterRules [ index ] ] = [ filterRules [ index ] , filterRules [ index + 1 ] ] ;
}
editingRuleId = " " ;
renderRules ( ) ;
applyFilters ( { redrawOnRemove : true } ) ;
currentProfileId = " " ;
profileSelect . value = " " ;
updateProfileControls ( ) ;
updateProfileSummary ( ) ;
updateUrlState ( ) ;
} ) ;
2026-05-19 01:46:23 +02:00
orientationActions . addEventListener ( " click " , ( event ) = > {
const button = event . target . closest ( " [data-orientation-action] " ) ;
if ( ! button ) return ;
applyOrientationContext ( button . dataset . orientationAction ) ;
} ) ;
2026-05-18 17:11:36 +02:00
document . querySelector ( " [data-action= ' fit ' ] " ) . addEventListener ( " click " , ( ) = > cy & & cy . fit ( cy . elements ( " :visible " ) , 48 ) ) ;
document . querySelector ( " [data-action= ' focus ' ] " ) . addEventListener ( " click " , applyFocus ) ;
document . querySelector ( " [data-action= ' clear ' ] " ) . addEventListener ( " click " , ( ) = > {
searchInput . value = " " ;
2026-05-18 18:31:25 +02:00
modeSelect . value = " full " ;
activeMode = " full " ;
2026-05-19 00:27:09 +02:00
labelSelect . value = " auto " ;
activeLabelMode = " auto " ;
2026-05-24 17:02:12 +02:00
zoneBoundaryToggle . checked = true ;
zoneGroupSelect . value = " deploymentEnvironment " ;
activeZoneGrouping = " deploymentEnvironment " ;
selectedZoneId = " " ;
2026-05-18 23:58:58 +02:00
setCheckedValues ( nodeTypeFilter ) ;
setCheckedValues ( edgeTypeFilter ) ;
2026-05-18 17:11:36 +02:00
reviewFilter . value = " " ;
unresolvedFilter . value = " " ;
focusSet = null ;
2026-05-18 20:14:07 +02:00
manualOverrides = { } ;
2026-05-19 01:16:18 +02:00
filterRules = [ ] ;
editingRuleId = " " ;
refreshRuleBuilder ( ) ;
renderRules ( ) ;
2026-05-18 20:14:07 +02:00
currentProfileId = " " ;
profileSelect . value = " " ;
2026-05-19 01:16:18 +02:00
applyFilters ( { redrawOnRemove : true } ) ;
2026-05-18 20:14:07 +02:00
updateProfileControls ( ) ;
updateProfileSummary ( ) ;
updateUrlState ( ) ;
2026-05-18 17:11:36 +02:00
} ) ;
document . querySelector ( " [data-action= ' reset-overrides ' ] " ) . addEventListener ( " click " , ( ) = > {
manualOverrides = { } ;
2026-05-18 20:14:07 +02:00
currentProfileId = " " ;
profileSelect . value = " " ;
2026-05-18 17:11:36 +02:00
applyFilters ( ) ;
2026-05-18 20:14:07 +02:00
updateProfileControls ( ) ;
updateProfileSummary ( ) ;
updateUrlState ( ) ;
2026-05-18 17:11:36 +02:00
} ) ;
document . querySelectorAll ( " [data-override] " ) . forEach ( ( button ) = > {
button . addEventListener ( " click " , ( ) = > {
if ( ! selected ) return ;
2026-05-18 20:14:07 +02:00
manualOverrides [ overrideKey ( selected ) ] = button . dataset . override ;
currentProfileId = " " ;
profileSelect . value = " " ;
2026-05-18 17:11:36 +02:00
applyFilters ( ) ;
2026-05-18 20:14:07 +02:00
updateProfileControls ( ) ;
updateProfileSummary ( ) ;
updateUrlState ( ) ;
} ) ;
} ) ;
profileSelect . addEventListener ( " input " , ( ) = > {
currentProfileId = profileSelect . value ;
const profile = selectedProfile ( ) ;
profileNameInput . value = profile ? profile . name : " " ;
if ( profile ) applyViewState ( profile . state | | { } ) ;
if ( ! profile ) updateUrlState ( ) ;
updateProfileControls ( ) ;
updateProfileSummary ( ) ;
} ) ;
document . querySelectorAll ( " [data-profile-action] " ) . forEach ( ( button ) = > {
button . addEventListener ( " click " , async ( ) = > {
if ( ! supportsLocalProfiles ( ) ) return ;
const action = button . dataset . profileAction ;
if ( action == = " save " ) {
saveCurrentProfile ( profileNameInput . value ) ;
} else if ( action == = " duplicate " ) {
const profile = selectedProfile ( ) ;
if ( ! profile ) return ;
saveCurrentProfile ( ` Copy of $ { profile . name } ` , { . . . profile , state : currentViewState ( ) } ) ;
} else if ( action == = " delete " ) {
profiles = profiles . filter ( ( profile ) = > profile . id != = currentProfileId ) ;
currentProfileId = " " ;
persistProfiles ( ) ;
renderProfiles ( ) ;
updateUrlState ( ) ;
} else if ( action == = " copy " ) {
const url = ` $ { window . location . origin } $ { viewUrl ( true ) } ` ;
if ( window . navigator ? . clipboard ? . writeText ) await window . navigator . clipboard . writeText ( url ) ;
updateProfileSummary ( " Copied the current map state URL. " ) ;
}
2026-05-18 17:11:36 +02:00
} ) ;
} ) ;
2026-05-19 00:51:38 +02:00
document . querySelectorAll ( " [data-help] " ) . forEach ( ( target ) = > {
target . addEventListener ( " mouseenter " , ( ) = > showHelp ( target ) ) ;
target . addEventListener ( " focus " , ( ) = > showHelp ( target ) ) ;
target . addEventListener ( " mouseleave " , hideHelp ) ;
target . addEventListener ( " blur " , hideHelp ) ;
} ) ;
document . addEventListener ( " keydown " , ( event ) = > {
if ( event . key == = " Escape " ) hideHelp ( ) ;
} ) ;
2026-05-18 17:11:36 +02:00
if ( ! window . cytoscape ) {
canvas . innerHTML = " <p class= ' notice ' >Cytoscape.js could not be loaded.</p> " ;
return ;
}
boot ( ) . catch ( ( error ) = > {
2026-05-19 00:51:38 +02:00
canvas . innerHTML = ` < p class = ' canvas-notice ' > Could not load graph explorer data : $ { escapeHtml ( error . message ) } < / p > ` ;
2026-05-18 17:11:36 +02:00
} ) ;
} ) ( ) ;
< / script >
< / body >
< / html >
"""