feat: render graph explorer zone boundaries

This commit is contained in:
2026-05-24 17:02:12 +02:00
parent ea81533172
commit 5a2d987b6a
4 changed files with 437 additions and 10 deletions

View File

@@ -154,6 +154,23 @@ The graph explorer should support zone-oriented modes for Fabric payloads:
Zone modes are diagnostic views. They answer "where does this run or who can
reach it here?" without mutating the underlying Fabric responsibility boundary.
Zone boundary overlays are a visual layer over the graph canvas. They should be
computed from visible node positions after layout/filtering rather than modeled
as graph parent nodes. The default boundary grouping is `deploymentEnvironment`:
| Overlay label | Matching nodes |
|---------------|----------------|
| `dev-tegwick` | visible nodes with `deploymentEnvironment: dev` in the private local development overlay |
| `test` | visible nodes with `deploymentEnvironment: test` or legacy `staging` |
| `prod` | visible nodes with `deploymentEnvironment: prod` |
Nodes without deployment overlay data are not enclosed. Edge-only deployment
overlay evidence may contribute to zone summaries and warnings, but it should
not create a boundary unless at least one visible node belongs to the same zone.
When access-zone grouping is selected, boundaries use `accessZone` values such
as `private-dev`, `collaborator-test`, `production-public`, or
`production-admin`.
Useful warnings for the graph explorer include:
- control surfaces in user-facing access zones;

View File

@@ -36,7 +36,50 @@ def graph_explorer_page() -> str:
}
.main { display: grid; grid-template-columns: minmax(0, 1fr) 340px; min-height: 0; }
.canvas-wrap { position: relative; min-width: 0; min-height: 0; }
#graph-canvas { position: absolute; inset: 0; }
#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: 2px solid var(--zone-color, #2563eb);
border-radius: 8px;
background: var(--zone-fill, rgba(37, 99, 235, .06));
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, .74), 0 10px 32px rgba(23, 32, 51, .08);
min-height: 56px;
min-width: 90px;
pointer-events: none;
}
.zone-boundary.selected {
border-width: 3px;
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, .82), 0 14px 36px rgba(23, 32, 51, .14);
}
.zone-label {
position: absolute;
top: 8px;
left: 10px;
z-index: 1;
min-height: 26px;
max-width: calc(100% - 20px);
border-color: var(--zone-color, #2563eb);
color: var(--zone-color, #2563eb);
background: rgba(255, 255, 255, .96);
box-shadow: 0 8px 18px rgba(23, 32, 51, .1);
font-size: 12px;
font-weight: 700;
overflow: hidden;
padding: 3px 8px;
pointer-events: auto;
text-overflow: ellipsis;
}
.zone-label:focus-visible {
outline: 2px solid var(--zone-color, #2563eb);
outline-offset: 2px;
}
.side {
border-left: 1px solid var(--line);
background: var(--panel);
@@ -181,6 +224,14 @@ def graph_explorer_page() -> str:
min-width: 84px;
padding: 4px 6px;
}
.map-control-field input[type="checkbox"] {
width: 18px;
height: 18px;
min-height: 0;
margin: 5px 0;
padding: 0;
accent-color: var(--accent);
}
.selection-anchor {
position: absolute;
z-index: 5;
@@ -390,7 +441,17 @@ def graph_explorer_page() -> str:
<main class="main">
<div class="canvas-wrap">
<div id="graph-canvas" role="img" aria-label="Interactive Fabric graph" aria-live="polite"><p class="canvas-notice">Loading graph...</p></div>
<div id="zone-overlay" class="zone-overlay" aria-label="Deployment zone boundaries"></div>
<div class="map-controls" aria-label="Map navigation controls">
<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>
<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>
<select id="label-select" title="Control node label density">
<option value="auto">Auto</option>
@@ -495,6 +556,7 @@ def graph_explorer_page() -> str:
const manifestUrl = "/exports/graph-explorer/manifest";
const graphUrl = "/exports/graph-explorer";
const canvas = document.getElementById("graph-canvas");
const zoneOverlay = document.getElementById("zone-overlay");
const popup = document.getElementById("popup");
const helpPopup = document.getElementById("help-popup");
const selectionAnchor = document.getElementById("selection-anchor");
@@ -503,6 +565,8 @@ def graph_explorer_page() -> str:
const modeSelect = document.getElementById("mode-select");
const layoutSelect = document.getElementById("layout-select");
const labelSelect = document.getElementById("label-select");
const zoneBoundaryToggle = document.getElementById("zone-boundary-toggle");
const zoneGroupSelect = document.getElementById("zone-group-select");
const nodeTypeFilter = document.getElementById("node-type-filter");
const nodeTypeSummary = document.getElementById("node-type-summary");
const edgeTypeFilter = document.getElementById("edge-type-filter");
@@ -541,12 +605,16 @@ def graph_explorer_page() -> str:
let allEdgeTypes = [];
let activeMode = "full";
let activeLabelMode = "auto";
let activeZoneGrouping = "deploymentEnvironment";
let profilePersistence = "none";
let profiles = [];
let currentProfileId = "";
let filterRules = [];
let editingRuleId = "";
let orientationContext = null;
let selectedZoneId = "";
let zoneOverlayFrame = 0;
let zoneSummaries = new Map();
const escapeHtml = (value) => String(value ?? "")
.replaceAll("&", "&amp;").replaceAll("<", "&lt;")
@@ -903,6 +971,255 @@ def graph_explorer_page() -> str:
return warnings;
};
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();
});
};
const groupCounts = (elements, field) => {
const counts = new Map();
elements.forEach((element) => {
@@ -927,7 +1244,7 @@ def graph_explorer_page() -> str:
const rows = [{label: "visible", value: `${visibleNodes} nodes / ${visibleEdges} edges`}];
fields.forEach((field) => {
const counts = groupCounts(visibleElements, field);
const counts = zoneCountsForField(visibleElements, field);
rows.push({
label: ruleAttributeLabels[field] || humanize(field),
value: counts.length
@@ -1032,6 +1349,8 @@ def graph_explorer_page() -> str:
edgeTypes: Array.from(selectedEdgeTypes()),
review: reviewFilter.value,
unresolved: unresolvedFilter.value,
zoneBoundaries: zoneBoundaryToggle ? zoneBoundaryToggle.checked : true,
zoneGrouping: zoneGroupSelect ? zoneGroupSelect.value || "deploymentEnvironment" : "deploymentEnvironment",
rules: filterRules.map((rule) => ({...rule})),
manualOverrides: {...manualOverrides},
});
@@ -1060,6 +1379,8 @@ def graph_explorer_page() -> str:
if (params.has("layer")) state.nodeTypes = parseListParam(params.get("layer"));
if (params.has("review")) state.review = params.get("review") || "";
if (params.has("unresolved")) state.unresolved = params.get("unresolved") || "";
if (params.has("zoneBoundaries")) state.zoneBoundaries = params.get("zoneBoundaries") !== "0";
if (params.has("zoneGrouping")) state.zoneGrouping = params.get("zoneGrouping") || "";
if (params.has("profile")) state.profile = params.get("profile") || "";
if (params.has("state")) {
try {
@@ -1082,6 +1403,8 @@ def graph_explorer_page() -> str:
if (state.edgeTypes.length !== allEdgeTypes.length) params.set("edgeTypes", state.edgeTypes.join(","));
if (state.review) params.set("review", state.review);
if (state.unresolved) params.set("unresolved", state.unresolved);
if (state.zoneBoundaries === false) params.set("zoneBoundaries", "0");
if (state.zoneGrouping && state.zoneGrouping !== "deploymentEnvironment") params.set("zoneGrouping", state.zoneGrouping);
if (currentProfileId) params.set("profile", currentProfileId);
if (includeStateBlob || hasManualOverrides() || filterRules.length > 0) {
params.set("state", encodeStateBlob(state));
@@ -1108,6 +1431,10 @@ def graph_explorer_page() -> str:
if ("edgeTypes" in state) setCheckedValues(edgeTypeFilter, (state.edgeTypes || []).filter((value) => allEdgeTypes.includes(value)));
if ("review" in state) reviewFilter.value = optionExists(reviewFilter, state.review) ? state.review : "";
if ("unresolved" in state) unresolvedFilter.value = optionExists(unresolvedFilter, state.unresolved) ? state.unresolved : "";
if ("zoneBoundaries" in state && zoneBoundaryToggle) zoneBoundaryToggle.checked = state.zoneBoundaries !== false;
if ("zoneGrouping" in state && zoneGroupSelect) {
zoneGroupSelect.value = optionExists(zoneGroupSelect, state.zoneGrouping) ? state.zoneGrouping : "deploymentEnvironment";
}
if ("manualOverrides" in state && state.manualOverrides && typeof state.manualOverrides === "object") {
manualOverrides = {...state.manualOverrides};
}
@@ -1119,6 +1446,8 @@ def graph_explorer_page() -> str:
}
activeMode = modeSelect.value || "full";
activeLabelMode = labelSelect.value || "auto";
activeZoneGrouping = zoneGroupSelect ? zoneGroupSelect.value || "deploymentEnvironment" : "deploymentEnvironment";
selectedZoneId = "";
focusSet = null;
syncFilterSummaries();
applyFilters();
@@ -1154,6 +1483,10 @@ def graph_explorer_page() -> str:
if (selectedEdgeTypes().size !== allEdgeTypes.length) parts.push("edge filter");
if (reviewFilter.value) parts.push(`${reviewFilter.value} review`);
if (unresolvedFilter.value) parts.push("unresolved only");
if (zoneBoundaryToggle && !zoneBoundaryToggle.checked) parts.push("zones off");
if (zoneGroupSelect && (zoneGroupSelect.value || "deploymentEnvironment") !== "deploymentEnvironment") {
parts.push("access zone boundaries");
}
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"}`);
@@ -1327,12 +1660,15 @@ def graph_explorer_page() -> str:
hiddenSummary.textContent = removed ? `Hidden ${hidden} / Removed ${removed}` : `Hidden ${hidden}`;
updateLabelVisibility();
updateSelectionAnchor();
scheduleZoneOverlayUpdate();
if (!selected) renderMapOverview();
if (options.redrawOnRemove && previousRemoved !== ruleRemovalSignature()) runLayout();
};
const showDetails = (element) => {
selected = element || null;
selectedZoneId = "";
scheduleZoneOverlayUpdate();
if (!element) {
orientationContext = null;
renderMapOverview();
@@ -1623,6 +1959,7 @@ def graph_explorer_page() -> str:
: {name, padding: 48, animate: false};
layoutElements.layout(options).run();
updateSelectionAnchor();
scheduleZoneOverlayUpdate();
};
const renderLegend = (layers) => {
@@ -1742,8 +2079,14 @@ def graph_explorer_page() -> str:
renderRules();
cy.on("tap", "node, edge", (event) => showDetails(event.target));
cy.on("tap", (event) => { if (event.target === cy) showDetails(null); });
cy.on("pan zoom resize render layoutstop", updateSelectionAnchor);
cy.on("position", "node", updateSelectionAnchor);
cy.on("pan zoom resize render layoutstop", () => {
updateSelectionAnchor();
scheduleZoneOverlayUpdate();
});
cy.on("position", "node", () => {
updateSelectionAnchor();
scheduleZoneOverlayUpdate();
});
cy.on("mouseover", "node, edge", (event) => {
event.target.addClass("hover");
const data = event.target.data();
@@ -1811,6 +2154,36 @@ def graph_explorer_page() -> str:
updateProfileSummary();
updateUrlState();
});
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();
});
ruleTarget.addEventListener("input", () => refreshRuleBuilder({target: ruleTarget.value}));
ruleType.addEventListener("input", () => refreshRuleBuilder({target: ruleTarget.value, type: ruleType.value}));
ruleAttribute.addEventListener("input", () => refreshRuleBuilder({
@@ -1899,6 +2272,10 @@ def graph_explorer_page() -> str:
activeMode = "full";
labelSelect.value = "auto";
activeLabelMode = "auto";
zoneBoundaryToggle.checked = true;
zoneGroupSelect.value = "deploymentEnvironment";
activeZoneGrouping = "deploymentEnvironment";
selectedZoneId = "";
setCheckedValues(nodeTypeFilter);
setCheckedValues(edgeTypeFilter);
reviewFilter.value = "";

View File

@@ -399,6 +399,9 @@ def test_registry_serves_graph_explorer_exports(tmp_path: Path) -> None:
assert 'id="mode-select"' in page
assert 'id="layout-select"' in page
assert 'id="label-select"' in page
assert 'id="zone-overlay"' in page
assert 'id="zone-boundary-toggle"' in page
assert 'id="zone-group-select"' in page
assert 'id="node-type-filter"' in page
assert 'id="edge-type-filter"' in page
assert 'id="rule-panel"' in page
@@ -411,6 +414,14 @@ def test_registry_serves_graph_explorer_exports(tmp_path: Path) -> None:
assert "ruleActionFor" in page
assert "ruleRemovalSignature" in page
assert "zoneModeFields" in page
assert "zoneForData" in page
assert "zoneBoundsForNodes" in page
assert "renderZoneOverlay" in page
assert "renderZoneDetails" in page
assert "dev-tegwick" in page
assert 'normalized === "staging"' in page
assert "zoneBoundaries" in page
assert "zoneGrouping" in page
assert "renderMapOverview" in page
assert "route without policy authority" in page
assert "deploymentEnvironment" in page

View File

@@ -4,7 +4,7 @@ type: workplan
title: "Zone Boundary Overlays"
domain: railiance
repo: railiance-fabric
status: ready
status: finished
owner: codex
topic_slug: railiance
created: "2026-05-24"
@@ -58,7 +58,7 @@ parent nodes unless testing shows the overlay layer is too fragile.
```task
id: RAIL-FAB-WP-0021-T01
status: todo
status: done
priority: high
state_hub_task_id: "91777c22-663c-443a-b682-38bfb7a864bf"
```
@@ -78,11 +78,16 @@ Expected grouping rules:
Done when the grouping and label rules are documented in the graph explorer
contract or operations docs and have focused tests.
Result: documented environment/access-zone boundary rules in
`docs/graph-explorer-contract.md`; graph explorer maps `dev` to `dev-tegwick`,
legacy `staging` to `test`, `prod` to `prod`, and ignores catch-all `all`
environment values for rectangle boundaries.
## T02 - Render Rectangle Boundary Overlays
```task
id: RAIL-FAB-WP-0021-T02
status: todo
status: done
priority: high
state_hub_task_id: "98ef5ecc-fa4d-443c-a4fe-fa896984d6c4"
```
@@ -103,11 +108,15 @@ Implementation expectations:
Done when the UI shows stable labeled rectangles for `dev-tegwick`, `test`, and
`prod` where matching nodes are visible.
Result: added a canvas overlay layer that computes padded rendered-node bounds,
draws labeled rectangles, updates after layout/pan/zoom/filter changes, and
hides empty zones.
## T03 - Add Zone Overlay Controls
```task
id: RAIL-FAB-WP-0021-T03
status: todo
status: done
priority: medium
state_hub_task_id: "30d48d5c-bd53-434b-9b8b-ed1a495cee4e"
```
@@ -125,11 +134,14 @@ Expected controls:
Done when operators can turn the overlay off for dense graph inspection and
restore it through shared/saved view state.
Result: added map controls for zone visibility and grouping by deployment
environment or access zone, with URL/profile state preservation.
## T04 - Surface Zone Boundary Details
```task
id: RAIL-FAB-WP-0021-T04
status: todo
status: done
priority: medium
state_hub_task_id: "c002aa54-a243-4912-81ae-0d282910ebc6"
```
@@ -148,11 +160,16 @@ Expected behavior:
Done when a zone label provides the same core answers as the WP-20 map overview,
but scoped to that zone.
Result: zone labels are clickable and show scoped counts for visible nodes,
environments, scenarios, access zones, routing authorities, policy authorities,
and route-without-policy-authority warnings; existing focus/highlight/hide
orientation actions work for zone contexts.
## T05 - Verify Responsive And Dense Graph Behavior
```task
id: RAIL-FAB-WP-0021-T05
status: todo
status: done
priority: high
state_hub_task_id: "03f265ad-f335-4caf-a882-35d8d30c75fc"
```
@@ -171,3 +188,8 @@ Checks should cover:
Done when automated tests cover the computed grouping behavior and a browser
smoke check confirms the canvas overlay is visible and aligned.
Result: static UI tests cover the zone controls/helpers; JavaScript syntax was
checked with `node --check`; the full test suite and validator pass; a headless
Edge smoke screenshot confirmed visible labeled boundaries in deployment
environment mode.