Fix release API proxy routes + wire pipeline to real data

- Add nginx proxy blocks for /api/v1/release-orchestrator/,
  /api/v1/release-control/, /api/v2/releases/, /api/v1/releases/,
  /api/v1/registries/ in Dockerfile.console
- All release UI calls now reach JobEngine (401 not 404)
- Registry search reaches Scanner service
- Pipeline page uses ReleaseManagementStore (real API, no mock data)
- Deployment wizard uses BundleOrganizerApi for create/seal
- Inline version/hotfix creation in deployment wizard wired to API
- Version detail shows "not found" error instead of blank screen
- Version wizard has promotion lane + duplicate component detection
- Sprint plan for 41 missing backend endpoints created

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
master
2026-03-23 15:38:16 +02:00
parent 66d84fb17a
commit d3353e9d16
10 changed files with 658 additions and 126 deletions

View File

@@ -211,9 +211,11 @@ internal static class ElkEdgeChannels
if (sinkBandsByEdgeId.ContainsKey(sorted[index].Id))
{
var sourceNode = positionedNodes[sorted[index].SourceNodeId];
var isGatewaySource = string.Equals(sourceNode.Kind, "Decision", StringComparison.OrdinalIgnoreCase)
|| string.Equals(sourceNode.Kind, "Fork", StringComparison.OrdinalIgnoreCase);
if (isGatewaySource)
var hasOtherForwardEdge = sorted.Length > 1
|| forwardEdgesBySource.TryGetValue(sorted[index].SourceNodeId, out var allSourceEdges)
&& allSourceEdges.Any(e => !sinkBandsByEdgeId.ContainsKey(e.Id));
var isNonChainSource = !string.Equals(sourceNode.Kind, "Task", StringComparison.OrdinalIgnoreCase);
if (hasOtherForwardEdge && isNonChainSource)
{
sinkBand = (-1, 0, 0d, double.NaN);
}

View File

@@ -80,10 +80,44 @@ internal static class ElkEdgeRouter
targetPoint = new ElkPoint { X = adjustedX, Y = targetNode.Y };
}
string? multiSideOverride = null;
if (string.IsNullOrWhiteSpace(edge.TargetPortId)
&& direction == ElkLayoutDirection.LeftToRight
&& (string.Equals(targetNode.Kind, "End", StringComparison.OrdinalIgnoreCase)
|| string.Equals(targetNode.Kind, "Join", StringComparison.OrdinalIgnoreCase)))
{
var sourceCenterY = sourceNode.Y + (sourceNode.Height / 2d);
var targetCenterY = targetNode.Y + (targetNode.Height / 2d);
var verticalOffset = sourceCenterY - targetCenterY;
var threshold = targetNode.Height * 0.4d;
if (verticalOffset < -threshold)
{
targetPoint = new ElkPoint { X = targetNode.X + (targetNode.Width / 2d), Y = targetNode.Y };
multiSideOverride = "NORTH";
}
else if (verticalOffset > threshold)
{
targetPoint = new ElkPoint { X = targetNode.X + (targetNode.Width / 2d), Y = targetNode.Y + targetNode.Height };
multiSideOverride = "SOUTH";
}
}
var bendPoints = direction == ElkLayoutDirection.LeftToRight
? ElkEdgeRouterBendPoints.BuildHorizontalBendPoints(sourceNode, targetNode, sourcePoint, targetPoint, graphBounds, channel, layerBoundariesByNodeId)
: ElkEdgeRouterBendPoints.BuildVerticalBendPoints(sourceNode, targetNode, sourcePoint, targetPoint, graphBounds, channel, layerBoundariesByNodeId);
if (multiSideOverride is not null)
{
var approachList = bendPoints is List<ElkPoint> list ? list : new List<ElkPoint>(bendPoints);
var approachOffset = 24d;
var approachX = targetNode.X + (targetNode.Width / 2d);
var approachY = multiSideOverride == "NORTH"
? targetNode.Y - approachOffset
: targetNode.Y + targetNode.Height + approachOffset;
approachList.Add(new ElkPoint { X = approachX, Y = approachY });
bendPoints = approachList;
}
var routedKind = channel.RouteMode == EdgeRouteMode.BackwardOuter
? $"backward|usc={channel.UseSourceCollector}|sox={channel.SharedOuterX:F0}"
: edge.Kind;
@@ -192,10 +226,17 @@ internal static class ElkEdgeRouter
{
if (positionedNodes.TryGetValue(dummyId, out var dummyPos))
{
var centerY = dummyPos.Y + (dummyPos.Height / 2d);
if (channel.RouteMode == EdgeRouteMode.Direct
&& (centerY > graphBounds.MaxY + 8d || centerY < graphBounds.MinY - 8d))
{
continue;
}
bendPoints.Add(new ElkPoint
{
X = dummyPos.X + (dummyPos.Width / 2d),
Y = dummyPos.Y + (dummyPos.Height / 2d),
Y = centerY,
});
}
}
@@ -213,6 +254,32 @@ internal static class ElkEdgeRouter
var targetAnchor = ElkEdgeRouterAnchors.ComputeSmartAnchor(targetNode, bendPoints.Count > 0 ? bendPoints[^1] : null,
false, targetEntryY, targetGroup?.Length ?? 1, direction);
if (direction == ElkLayoutDirection.LeftToRight
&& (string.Equals(targetNode.Kind, "End", StringComparison.OrdinalIgnoreCase)
|| string.Equals(targetNode.Kind, "Join", StringComparison.OrdinalIgnoreCase)))
{
var sourceCenterY = sourceNode.Y + (sourceNode.Height / 2d);
var targetCenterY = targetNode.Y + (targetNode.Height / 2d);
var verticalOffset = sourceCenterY - targetCenterY;
var sideThreshold = targetNode.Height * 0.4d;
if (verticalOffset < -sideThreshold)
{
targetAnchor = ElkEdgeRouterAnchors.ResolvePreferredAnchorPoint(
targetNode, sourceNode.X + sourceNode.Width, targetNode.Y - 256d, "NORTH", direction);
var approachOffset = 24d;
var approachX = targetNode.X + (targetNode.Width / 2d);
bendPoints.Add(new ElkPoint { X = approachX, Y = targetNode.Y - approachOffset });
}
else if (verticalOffset > sideThreshold)
{
targetAnchor = ElkEdgeRouterAnchors.ResolvePreferredAnchorPoint(
targetNode, sourceNode.X + sourceNode.Width, targetNode.Y + targetNode.Height + 256d, "SOUTH", direction);
var approachOffset = 24d;
var approachX = targetNode.X + (targetNode.Width / 2d);
bendPoints.Add(new ElkPoint { X = approachX, Y = targetNode.Y + targetNode.Height + approachOffset });
}
}
reconstructed[edge.Id] = new ElkRoutedEdge
{
Id = edge.Id,

View File

@@ -222,8 +222,8 @@ internal static class ElkEdgeRouterBendPoints
var targetApproachX = Math.Max(sourceExitX + 24d, targetBoundary.MinX - 32d);
var outerY = graphBounds.MaxY + 32d + ElkEdgeChannelBands.ResolveSinkBandOffset(Math.Max(0, channel.SinkBandIndex));
var horizontalSpan = targetApproachX - sourceExitX;
if (horizontalSpan > 200d)
var corridorSpan = targetApproachX - sourceExitX;
if (corridorSpan > 200d)
{
return ElkLayoutHelpers.NormalizeBendPoints(
new ElkPoint { X = targetApproachX, Y = startPoint.Y },
@@ -256,8 +256,8 @@ internal static class ElkEdgeRouterBendPoints
? graphBounds.MinY - 56d - ElkEdgeChannelBands.ResolveSinkBandOffset(Math.Max(0, channel.SinkBandIndex), 36d, 28d)
: channel.PreferredOuterY + ElkEdgeChannelBands.ResolveSinkBandOffset(Math.Max(0, channel.SinkBandIndex), 28d, 24d);
var topHorizontalSpan = targetApproachX - sourceExitX;
if (topHorizontalSpan > 200d)
var topCorridorSpan = targetApproachX - sourceExitX;
if (topCorridorSpan > 200d)
{
return ElkLayoutHelpers.NormalizeBendPoints(
new ElkPoint { X = targetApproachX, Y = startPoint.Y },

View File

@@ -0,0 +1,24 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.5.2.0
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StellaOps.ElkSharp", "StellaOps.ElkSharp.csproj", "{C14F76B1-AFCA-BAF5-D682-EFA322DF1D6E}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{C14F76B1-AFCA-BAF5-D682-EFA322DF1D6E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C14F76B1-AFCA-BAF5-D682-EFA322DF1D6E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C14F76B1-AFCA-BAF5-D682-EFA322DF1D6E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C14F76B1-AFCA-BAF5-D682-EFA322DF1D6E}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {4342DA81-8C0D-4763-BDF9-77353C422BA9}
EndGlobalSection
EndGlobal