185 Commits

Author SHA1 Message Date
hsiegeln
ebe768711b fix: Cmd-K exchange selection reads exchangeId from URL params
All checks were successful
CI / cleanup-branch (push) Has been skipped
CI / build (push) Successful in 1m1s
CI / docker (push) Successful in 57s
CI / deploy-feature (push) Has been skipped
CI / deploy (push) Successful in 36s
ExchangesPage ignored the exchangeId URL parameter, so selecting an
exchange from the command palette navigated to the right URL but never
displayed the execution overlay. Now derives selection from URL params
as fallback, and LayoutShell passes selectedExchange in state for
exchange/attribute results.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 15:26:36 +02:00
hsiegeln
af45f93854 fix: add missing isReplay parameter to test constructors
All checks were successful
CI / cleanup-branch (push) Has been skipped
CI / build (push) Successful in 1m1s
CI / docker (push) Successful in 57s
CI / deploy-feature (push) Has been skipped
CI / deploy (push) Successful in 41s
The ExecutionDocument and ExecutionRecord records gained an isReplay
field but the integration tests were not updated, breaking CI.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 15:08:12 +02:00
hsiegeln
da1d74309e fix: detect replay via replayExchangeId field, not just header
Some checks failed
CI / cleanup-branch (push) Has been skipped
CI / build (push) Failing after 1m4s
CI / docker (push) Has been skipped
CI / deploy (push) Has been skipped
CI / deploy-feature (push) Has been skipped
The X-Cameleer-Replay header is only available when inputSnapshot is
captured (DETAILED/DEEP engine level). The agent always sets
replayExchangeId on RouteExecution, so check that first.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 14:57:59 +02:00
hsiegeln
7a4d7b6915 fix: resolve 8 SonarQube reliability bugs
Some checks failed
CI / cleanup-branch (push) Has been skipped
CI / build (push) Failing after 1m2s
CI / docker (push) Has been skipped
CI / deploy (push) Has been skipped
CI / deploy-feature (push) Has been skipped
- ElkDiagramRenderer: guard against null containingNode before getElkRoot()
- OpenSearchAdminController: return 503/502 instead of 200 on errors
- DatabaseAdminController: return 503 instead of 200 on connection failure
- SpaForwardController: replace unbound {path} variables with /** wildcards
- WriteBuffer: check offer() return value and log on unexpected rejection
- ApiExceptionHandler: extract getReason() to local var for null safety
- Admin UI pages: handle isError state for disconnected service display

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 14:39:54 +02:00
hsiegeln
ab7031e6ed feat: add is_replay flag to execution pipeline and UI
Detect replayed exchanges via X-Cameleer-Replay header during ingestion,
persist the flag through PostgreSQL and OpenSearch, and surface it in
the dashboard (amber replay icon) and exchange detail chain view.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 14:39:40 +02:00
hsiegeln
cf3cec0164 feat: show replay marker on correlated chain entries
Some checks failed
CI / cleanup-branch (push) Has been skipped
CI / build (push) Successful in 1m46s
CI / docker (push) Successful in 1m52s
CI / deploy (push) Successful in 51s
CI / deploy-feature (push) Has been skipped
SonarQube / sonarqube (push) Failing after 1m16s
Exchanges with a _replay attribute now display a small amber
RotateCcw icon between the status dot and route name in the
correlation chain. Tooltip also indicates (replay).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-30 23:26:55 +02:00
hsiegeln
79762c3f0d fix: audit replay with actual outcome, not premature SUCCESS
All checks were successful
CI / build (push) Successful in 2m8s
CI / cleanup-branch (push) Has been skipped
CI / docker (push) Successful in 1m7s
CI / deploy-feature (push) Has been skipped
CI / deploy (push) Successful in 42s
Replay audit log now records the agent's reply status (SUCCESS/FAILURE),
message, and error details. Timeout and internal errors are also logged
as FAILURE with the cause.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-30 23:14:36 +02:00
hsiegeln
715cbc1894 feat: synchronous replay endpoint with agent response status
All checks were successful
CI / cleanup-branch (push) Has been skipped
CI / build (push) Successful in 1m8s
CI / docker (push) Successful in 56s
CI / deploy-feature (push) Has been skipped
CI / deploy (push) Successful in 38s
Add dedicated POST /agents/{id}/replay endpoint that uses
addCommandWithReply to wait for the agent ACK (30s timeout).
Returns the actual replay result (status, message, data) instead
of just a delivery confirmation.

Frontend toast now reflects the agent's response: "Replay completed"
on success, agent error message on failure, timeout message if the
agent doesn't respond.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-30 22:48:02 +02:00
hsiegeln
dd398178f0 docs: add route-control command to HOWTO and CLAUDE.md
All checks were successful
CI / cleanup-branch (push) Has been skipped
CI / build (push) Successful in 1m35s
CI / docker (push) Successful in 13s
CI / deploy (push) Successful in 49s
CI / deploy-feature (push) Has been skipped
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-30 21:44:12 +02:00
hsiegeln
8b0d473fcd feat: add route control bar and fix replay protocol compliance
Some checks failed
CI / cleanup-branch (push) Has been skipped
CI / build (push) Successful in 1m4s
CI / docker (push) Successful in 1m0s
CI / deploy-feature (push) Has been skipped
CI / deploy (push) Has been cancelled
Add ROUTE_CONTROL command type and route-control mapping in
AgentCommandController. New RouteControlBar component in the exchange
header shows Start/Stop/Suspend/Resume actions (grouped pill bar) and
a Replay button, gated by agent capabilities and OPERATOR/ADMIN role.

Fix useReplayExchange hook to match protocol section 16: payload now
uses { routeId, exchange: { body, headers }, originalExchangeId, nonce }
instead of the flat { headers, body } format.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-30 21:42:06 +02:00
hsiegeln
30e9b55379 fix: detail panel respects iteration filtering
All checks were successful
CI / cleanup-branch (push) Has been skipped
CI / build (push) Successful in 1m35s
CI / docker (push) Successful in 1m12s
CI / deploy-feature (push) Has been skipped
CI / deploy (push) Successful in 48s
- findProcessorInTree now skips non-selected iteration wrappers so
  the returned ProcessorNode has data from the correct iteration
- Gate selectedProcessor on overlay presence so processors not
  executed in the current iteration don't show in the detail panel
- Header shows "Exchange Details" or "Processor Details" contextually

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-30 20:25:28 +02:00
hsiegeln
3091754b0f fix: dim compound containers when no descendants executed in overlay
CompoundNode (circuit breaker, choice, etc.) now renders at 0.35
opacity when the overlay is active but neither the compound itself
nor any of its diagram descendants appear in the execution overlay.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-30 20:13:40 +02:00
hsiegeln
26de222884 refactor: move config badges inline, fix trace config from server
All checks were successful
CI / cleanup-branch (push) Has been skipped
CI / build (push) Successful in 1m1s
CI / docker (push) Successful in 56s
CI / deploy-feature (push) Has been skipped
CI / deploy (push) Successful in 40s
- Render hasTrace/hasTap/status badges inside the node card in both
  raw diagram and overlay modes (consistent positioning)
- Pulse only on trace badge in overlay mode when hasTraceData is true
- Fix nodeConfigs to read tracedProcessors from appConfig instead of
  never-synced tracing store

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-30 18:08:40 +02:00
hsiegeln
2f2f93f37e fix: move useCallback before early returns to fix hooks order
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-30 17:47:17 +02:00
hsiegeln
1b9a3b84a0 feat: add JSON download button to execution diagram
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-30 17:43:02 +02:00
hsiegeln
c77de4a232 fix: simplify detail panel header to just "Details"
Remove redundant processor name, status, ID, and duration from the
header bar — all visible in the Info tab and diagram overlay already.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-30 16:40:18 +02:00
hsiegeln
15b8c09e17 fix: position resolved URI directly below text lines in diagram overlay
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-30 16:35:28 +02:00
hsiegeln
77e87504d6 feat: agent row click navigates to detail page instead of slide-in
All checks were successful
CI / cleanup-branch (push) Has been skipped
CI / build (push) Successful in 1m2s
CI / docker (push) Successful in 55s
CI / deploy-feature (push) Has been skipped
CI / deploy (push) Successful in 40s
Replace DetailPanel overlay with direct navigation to
/runtime/:appId/:instanceId on row click. Removes the slide-in panel,
AgentOverviewContent, and AgentPerformanceContent helper components.
The full AgentInstance page already provides all the same data plus
more (charts, routes, logs).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-30 16:28:12 +02:00
hsiegeln
d8a21f0724 feat: GitHub-style contribution grid for punchcard heatmap
All checks were successful
CI / cleanup-branch (push) Has been skipped
CI / build (push) Successful in 1m3s
CI / docker (push) Successful in 54s
CI / deploy-feature (push) Has been skipped
CI / deploy (push) Successful in 36s
Replace Recharts ScatterChart with compact SVG grid of small rounded
squares (11x11px, 2px gap). 7 rows (Mon-Sun) x 24 columns (hours).
Color intensity = value relative to max. Transactions = blue scale,
Errors = red scale. Toggle switches between modes.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-30 15:49:45 +02:00
hsiegeln
4a91ca0774 feat: consolidate punchcard heatmaps into single toggle component
All checks were successful
CI / cleanup-branch (push) Has been skipped
CI / build (push) Successful in 1m1s
CI / docker (push) Successful in 54s
CI / deploy-feature (push) Has been skipped
CI / deploy (push) Successful in 37s
Replace two separate Transaction/Error punchcard cards with a single
card containing a Transactions/Errors toggle. Uses internal state to
switch between modes without remounting the chart.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-30 15:45:22 +02:00
hsiegeln
52c22f1eb9 fix: dashboard flickering on poll, animation replay, and scroll
All checks were successful
CI / cleanup-branch (push) Has been skipped
CI / build (push) Successful in 1m3s
CI / docker (push) Successful in 54s
CI / deploy-feature (push) Has been skipped
CI / deploy (push) Successful in 36s
- Add placeholderData to useRouteMetrics and usePunchcard hooks so data
  stays stable between refetches instead of going undefined → flicker
- Disable Recharts animation on Treemap (isAnimationActive=false)
- Make .content scrollable (overflow-y: auto, flex: 1, min-height: 0)
  so charts below the fold are accessible

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-30 15:42:02 +02:00
hsiegeln
a517785050 chore: regenerate OpenAPI types and remove type assertion hacks
All checks were successful
CI / cleanup-branch (push) Has been skipped
CI / build (push) Successful in 1m2s
CI / docker (push) Successful in 56s
CI / deploy-feature (push) Has been skipped
CI / deploy (push) Successful in 39s
Regenerated schema.d.ts from live backend — now includes slaCompliance
on ExecutionStats/RouteMetrics, filterMatched/duplicateMessage on
ProcessorNode, and all new dashboard endpoints (timeseries/by-app,
timeseries/by-route, punchcard, errors/top, app-settings).

Removed Record<string, unknown> casts that were working around the
stale schema.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-30 15:36:44 +02:00
hsiegeln
474738a894 fix: resolve TypeScript strict mode errors failing CI
All checks were successful
CI / cleanup-branch (push) Has been skipped
CI / build (push) Successful in 1m4s
CI / docker (push) Successful in 1m25s
CI / deploy-feature (push) Has been skipped
CI / deploy (push) Successful in 41s
- StatusDot: status → variant (correct prop name)
- Badge: color="muted" → color="auto" (valid BadgeColor)
- AreaChart: remove stacked prop (not in AreaChartProps)
- DataTable: remove defaultSort prop (not in DataTableProps)
- TopError → ErrorRow with id field (DataTable requires T extends {id})
- slaCompliance: type assertion for runtime field not in TS schema
- PunchcardHeatmap Scatter shape: proper typing for custom renderer

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-30 15:26:26 +02:00
hsiegeln
41397ae067 feat: migrate Treemap and PunchcardHeatmap to Recharts
Some checks failed
CI / cleanup-branch (push) Has been skipped
CI / build (push) Failing after 31s
CI / docker (push) Has been skipped
CI / deploy (push) Has been skipped
CI / deploy-feature (push) Has been skipped
Replace custom SVG chart implementations with Recharts components:
- Treemap: uses Recharts Treemap with custom content renderer for
  SLA-colored cells, labels, and click navigation
- PunchcardHeatmap: uses Recharts ScatterChart with custom Rectangle
  shape for weekday x hour heatmap grid cells

Both use ResponsiveContainer (no more explicit width/height props) and
rechartsTheme from the design system for consistent tooltip styling.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-30 15:20:29 +02:00
hsiegeln
dd91a4989b chore: update @cameleer/design-system to v0.1.21
Some checks failed
CI / build (push) Failing after 43s
CI / cleanup-branch (push) Has been skipped
CI / docker (push) Has been skipped
CI / deploy (push) Has been skipped
CI / deploy-feature (push) Has been skipped
New exports: rechartsTheme (pre-configured Recharts prop objects matching
design system styling), CHART_COLORS (series color palette), and properly
exported ChartSeries/DataPoint interfaces. No breaking changes.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-30 15:03:27 +02:00
hsiegeln
f06f5f2bb1 docs: add CSS variable rule to CLAUDE.md
Some checks failed
CI / cleanup-branch (push) Has been skipped
CI / build (push) Failing after 26s
CI / docker (push) Has been skipped
CI / deploy (push) Has been skipped
CI / deploy-feature (push) Has been skipped
Always use design system CSS variables for colors, never hardcode hex.
Applies to CSS modules, inline styles, and SVG fill/stroke attributes.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-30 11:00:53 +02:00
hsiegeln
c8caf3dc44 fix: use CSS variables directly for gate state colors
Some checks failed
CI / cleanup-branch (push) Has been skipped
CI / build (push) Failing after 25s
CI / docker (push) Has been skipped
CI / deploy (push) Has been skipped
CI / deploy-feature (push) Has been skipped
Use var(--amber) and var(--amber-bg) in SVG fill/stroke attributes
instead of hardcoded hex values. SVG presentation attributes resolve
CSS variables correctly, and this respects dark mode theme switching.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-30 10:59:07 +02:00
hsiegeln
2de10f6eb0 fix: use theme amber colors for gate state instead of arbitrary hex
Some checks failed
CI / cleanup-branch (push) Has been skipped
CI / build (push) Failing after 26s
CI / docker (push) Has been skipped
CI / deploy (push) Has been skipped
CI / deploy-feature (push) Has been skipped
Use --amber (#C6820E) and --amber-bg (#FDF6E9) from the design system
theme instead of hardcoded #D97706/#FFFBEB.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-30 10:55:59 +02:00
hsiegeln
e2c0f203f9 feat: amber container for filter/idempotent gate state + red pulse on failed badge
Some checks failed
CI / cleanup-branch (push) Has been skipped
CI / build (push) Failing after 29s
CI / docker (push) Has been skipped
CI / deploy (push) Has been skipped
CI / deploy-feature (push) Has been skipped
When a filter processor rejects a message (filterMatched=false) or an
idempotent consumer detects a duplicate (duplicateMessage=true), the
compound container turns amber (header, border, body tint).

Also adds red pulsing rings on the failed processor badge (same SMIL
pattern as the teal hasTraceData pulse).

Backend: ProcessorNode gains filterMatched/duplicateMessage fields,
threaded from ProcessorExecution JSON path.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-30 10:53:57 +02:00
hsiegeln
a383b9bcf4 feat: add red pulse effect to failed processor badges in diagram overlay
Failed processor nodes now show expanding/fading red rings around the
error badge (same SMIL animation pattern as the teal hasTraceData pulse).
Two staggered circles expand from r=6 to r=14 over 1.5s, making failures
immediately visible in complex route diagrams.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-30 10:42:35 +02:00
hsiegeln
6aeba1fe83 fix: side-by-side layout for treemap and punchcard heatmaps
Some checks failed
CI / cleanup-branch (push) Has been skipped
CI / build (push) Failing after 29s
CI / docker (push) Has been skipped
CI / deploy (push) Has been skipped
CI / deploy-feature (push) Has been skipped
Treemap on left (3fr), two punchcards stacked on right (2fr) using
new .vizRow grid layout. Replaces full-width stacked arrangement.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-30 10:32:23 +02:00
hsiegeln
7a1625c297 fix: make treemap and punchcard responsive with viewBox scaling
Replaced hardcoded width/height on SVG elements with viewBox + width:100%
so both components fill their parent container instead of using fixed pixels.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-30 10:28:29 +02:00
hsiegeln
9d2d87e7e1 feat: add treemap and punchcard heatmap to dashboard L1/L2 (#94)
Treemap: rectangle area = transaction volume, color = SLA compliance
(green→red). Shows apps at L1, routes at L2. Click navigates deeper.

Punchcard heatmap: 7-day rolling weekday x 24-hour grid showing
transaction volume and error patterns. Two side-by-side views
(transactions + errors) reveal temporal clustering.

Backend: new GET /search/stats/punchcard endpoint aggregating
stats_1m_all/app by DOW x hour over rolling 7 days.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-30 10:26:26 +02:00
hsiegeln
b5c19b6774 feat: latency heatmap overlay on process diagram (#94)
Some checks failed
CI / cleanup-branch (push) Has been skipped
CI / build (push) Failing after 29s
CI / docker (push) Has been skipped
CI / deploy (push) Has been skipped
CI / deploy-feature (push) Has been skipped
SonarQube / sonarqube (push) Failing after 1m10s
Add latencyHeatmap prop to ProcessDiagram that colors nodes green→yellow→red
based on their relative contribution to route latency (pctOfRoute). Shows avg
duration label on each node. Threaded through CompoundNode for nested EIP
patterns. Heatmap is active only when no execution overlay is present.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 23:32:42 +02:00
hsiegeln
213aa86c47 feat: progressive drill-down dashboard with RED metrics and SLA compliance (#94)
Three-level dashboard driven by sidebar selection:
- L1 (no selection): all-apps overview with health table, per-app charts
- L2 (app selected): route performance table, error velocity, top errors
- L3 (route selected): processor table, latency heatmap data, bottleneck KPI

Backend: 3 new endpoints (timeseries/by-app, timeseries/by-route, errors/top),
per-app SLA settings (app_settings table, V12 migration), exact SLA compliance
from executions hypertable, error velocity with acceleration detection.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 23:29:20 +02:00
hsiegeln
b2ae37637d fix: update diagram tests for new cameleer3-common without flat nodes list
All checks were successful
CI / cleanup-branch (push) Has been skipped
CI / build (push) Successful in 1m3s
CI / docker (push) Successful in 1m0s
CI / deploy-feature (push) Has been skipped
CI / deploy (push) Successful in 51s
RouteGraph no longer stores a separate nodes list; getNodes() computes
from root tree. Tests now build proper tree via setRoot() + setChildren()
instead of calling setNodes().

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 19:41:04 +02:00
hsiegeln
7e968dc06b fix: use root tree for compound node detection instead of flat nodes list
Some checks failed
CI / cleanup-branch (push) Has been skipped
CI / build (push) Failing after 1m4s
CI / docker (push) Has been skipped
CI / deploy (push) Has been skipped
CI / deploy-feature (push) Has been skipped
The agent now sends shallow copies (without children) in the flat nodes
list. Build nodeById map by walking graph.getRoot() tree which preserves
children, falling back to flat list via putIfAbsent for compatibility.

Also adds EIP_FILTER, EIP_IDEMPOTENT_CONSUMER, EIP_RECIPIENT_LIST as
new compound container types per updated DIAGRAMS.md.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 19:34:40 +02:00
hsiegeln
0ec41bc02c docs: add dashboard design spec
Some checks failed
CI / cleanup-branch (push) Has been skipped
CI / docker (push) Has been cancelled
CI / deploy (push) Has been cancelled
CI / deploy-feature (push) Has been cancelled
CI / build (push) Has been cancelled
Progressive drill-down dashboard following RED method (Rate, Errors,
Duration) with 3 scope levels driven by sidebar selection.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 19:02:35 +02:00
hsiegeln
59ddbb65b9 revert: re-apply EIP_CIRCUIT_BREAKER compound rendering
All checks were successful
CI / cleanup-branch (push) Has been skipped
CI / build (push) Successful in 1m2s
CI / docker (push) Successful in 1m1s
CI / deploy-feature (push) Has been skipped
CI / deploy (push) Successful in 37s
Restores e8039f9. The compound rendering regression was caused by
the agent sending flat nodes without children, not the renderer code.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 18:22:43 +02:00
hsiegeln
673f0958c5 revert: temporarily revert EIP_CIRCUIT_BREAKER compound rendering
All checks were successful
CI / cleanup-branch (push) Has been skipped
CI / build (push) Successful in 1m4s
CI / docker (push) Successful in 58s
CI / deploy-feature (push) Has been skipped
CI / deploy (push) Successful in 38s
Reverting e8039f9 to diagnose compound rendering regression affecting
all compound types (SPLIT, CHOICE, LOOP, DO_TRY) and error handlers.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 18:15:10 +02:00
hsiegeln
e8039f9cc4 feat: render EIP_CIRCUIT_BREAKER as compound container with main/fallback lanes
All checks were successful
CI / cleanup-branch (push) Has been skipped
CI / build (push) Successful in 1m1s
CI / docker (push) Successful in 58s
CI / deploy-feature (push) Has been skipped
CI / deploy (push) Successful in 41s
Follow the DO_TRY pattern: virtual _CB_MAIN wrapper for main path children,
onFallback rendered as _CB_FALLBACK section with purple dashed border.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 17:40:28 +02:00
hsiegeln
9eb2c2692b fix: render continuation edges exiting compound nodes (SPLIT, CHOICE)
All checks were successful
CI / cleanup-branch (push) Has been skipped
CI / build (push) Successful in 1m2s
CI / docker (push) Successful in 42s
CI / deploy-feature (push) Has been skipped
CI / deploy (push) Successful in 37s
The cross-root boundary check in createElkEdges() was too aggressive,
skipping all edges where source and target have different ELK roots.
Compound nodes are their own ELK roots, so valid continuation edges
from the last child inside a compound to the next sibling were lost.

Now allows edges when nodes share a common grandparent or when one
node exits/enters a compound boundary.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 16:39:14 +02:00
hsiegeln
090c51c809 feat: resolved URI display and drill-down for TO/TO_DYNAMIC nodes
All checks were successful
CI / cleanup-branch (push) Has been skipped
CI / build (push) Successful in 1m2s
CI / docker (push) Successful in 55s
CI / deploy-feature (push) Has been skipped
CI / deploy (push) Successful in 36s
- Show resolved endpoint URI as teal italic line on diagram nodes
  when execution overlay is active
- Enable drill-down for TO and TO_DYNAMIC nodes (not just DIRECT/SEDA)
- Use runtime resolvedEndpointUri from execution overlay for drill-down
  when static endpointUri doesn't match
- Increase node height from 50px to 56px to accommodate the third line

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 16:30:11 +02:00
hsiegeln
32cde5363f fix: show resolvedEndpointUri in info tab, reflect trace/tap state in toolbar
All checks were successful
CI / cleanup-branch (push) Has been skipped
CI / build (push) Successful in 1m40s
CI / docker (push) Successful in 1m49s
CI / deploy (push) Successful in 53s
CI / deploy-feature (push) Has been skipped
- Info tab now reads processor.resolvedEndpointUri instead of hardcoded "-"
- Toolbar buttons highlight in teal/purple when trace/tap is active
- Tooltip changes to "Disable tracing" / "Edit tap" when active

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 15:45:06 +02:00
hsiegeln
604e5db874 fix: write has_trace_data to OpenSearch document during indexing
All checks were successful
CI / cleanup-branch (push) Has been skipped
CI / build (push) Successful in 1m2s
CI / docker (push) Successful in 38s
CI / deploy-feature (push) Has been skipped
CI / deploy (push) Successful in 36s
The toMap() method was missing the has_trace_data field, so it was
never indexed despite being read back in hitToSummary().

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 14:39:57 +02:00
hsiegeln
a4fcb8810f fix: use actual lucide Footprints icon for trace badges
All checks were successful
CI / cleanup-branch (push) Has been skipped
CI / build (push) Successful in 1m2s
CI / docker (push) Successful in 51s
CI / deploy-feature (push) Has been skipped
CI / deploy (push) Successful in 36s
Replace hand-drawn teardrop paths (looked like plants) with the real
lucide Footprints SVG paths. Configured = bare teal icon, data captured
= white icon in solid teal circle with staggered pulse rings.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 13:49:36 +02:00
hsiegeln
3d71345181 feat: trace data indicators, inline tap config, and detail tab gating
All checks were successful
CI / build (push) Successful in 1m46s
CI / cleanup-branch (push) Has been skipped
CI / docker (push) Successful in 1m25s
CI / deploy-feature (push) Has been skipped
CI / deploy (push) Successful in 1m57s
Trace data visibility:
- ProcessorNode now includes hasTraceData flag computed from captured
  body/headers during tree conversion
- ConfigBadge shows teal for tracing configured, green when data captured
- Search results show green footprints icon for exchanges with trace data
- New has_trace_data column on executions table (V11 migration with backfill)
- OpenSearch documents and ExecutionSummary include the flag

Inline tap configuration:
- Extracted reusable TapConfigModal component from RouteDetail
- Diagram context menu opens tap modal inline instead of navigating away
- Toggle-trace action works immediately with toast feedback
- Modal closes only on ESC, Cancel, Save, or Delete (not backdrop click)

Detail panel tab gating:
- Headers, Input, Output tabs disabled when no data is available
- Works at both exchange and processor level
- Falls back to Info tab when active tab becomes empty

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 13:08:58 +02:00
hsiegeln
5103f40196 feat: replace Unicode diagram icons with lucide SVG icons
All checks were successful
CI / cleanup-branch (push) Has been skipped
CI / build (push) Successful in 58s
CI / docker (push) Successful in 54s
CI / deploy-feature (push) Has been skipped
CI / deploy (push) Successful in 37s
Each of the ~40 node types now has a distinct, semantically meaningful
lucide icon rendered as crisp SVG paths. Compound node headers also
show their icon left-aligned in the header bar.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 11:56:19 +02:00
hsiegeln
09a60c5a6c feat: add camel logo and random desert-themed subtitles to login page
All checks were successful
CI / cleanup-branch (push) Has been skipped
CI / build (push) Successful in 59s
CI / docker (push) Successful in 52s
CI / deploy-feature (push) Has been skipped
CI / deploy (push) Successful in 36s
25 rotating cameleer-themed login subtitles picked randomly on each
page load. Also adds the camel logo SVG next to the app name.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 11:10:27 +02:00
hsiegeln
7a84914866 fix: use cameleer logo as favicon, upgrade design system to v0.1.20, fix DataTable scroll
All checks were successful
CI / cleanup-branch (push) Has been skipped
CI / build (push) Successful in 57s
CI / docker (push) Successful in 1m16s
CI / deploy-feature (push) Has been skipped
CI / deploy (push) Successful in 37s
- Replace placeholder clock favicon with cameleer camel logo SVG
- Upgrade @cameleer/design-system from v0.1.19 to v0.1.20
- Add minHeight: 0 to main element to complete flex chain for fillHeight DataTable

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 11:02:32 +02:00
hsiegeln
88c51b75bf docs: mark design system update instructions as done in v0.1.19
Some checks failed
CI / cleanup-branch (push) Has been skipped
CI / build (push) Successful in 56s
CI / docker (push) Successful in 10s
CI / deploy-feature (push) Has been skipped
CI / deploy (push) Successful in 35s
SonarQube / sonarqube (push) Failing after 1m57s
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 22:20:34 +01:00
hsiegeln
3f87f37095 fix: register JavaTimeModule on DetailService ObjectMapper
All checks were successful
CI / cleanup-branch (push) Has been skipped
CI / build (push) Successful in 58s
CI / docker (push) Successful in 40s
CI / deploy-feature (push) Has been skipped
CI / deploy (push) Successful in 37s
Same issue as IngestionService — the ObjectMapper deserializing
processors_json lacked JavaTimeModule, causing Instant parsing to fail
silently and falling back to the broken flat reconstruction.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 22:12:06 +01:00
hsiegeln
ac4476ccd6 fix: register JavaTimeModule on ObjectMapper for processors_json serialization
All checks were successful
CI / cleanup-branch (push) Has been skipped
CI / build (push) Successful in 1m1s
CI / docker (push) Successful in 40s
CI / deploy-feature (push) Has been skipped
CI / deploy (push) Successful in 51s
The ObjectMapper used to serialize the processor tree to JSON lacked
JavaTimeModule, causing Instant fields (startTime, endTime) to fail
silently — processors_json was always null.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 22:02:05 +01:00
hsiegeln
30344d29b1 feat: store raw processor tree JSON and add error categorization fields
All checks were successful
CI / cleanup-branch (push) Has been skipped
CI / build (push) Successful in 1m0s
CI / docker (push) Successful in 53s
CI / deploy-feature (push) Has been skipped
CI / deploy (push) Successful in 37s
Fixes iteration overlay corruption caused by flat storage collapsing
duplicate processorIds across loop iterations.

Server:
- Store raw processor tree as processors_json JSONB on executions table
- Detail endpoint serves from processors_json (faithful tree), falls back
  to flat record reconstruction for older executions
- V10 migration: processors_json, error categorization (errorType,
  errorCategory, rootCauseType, rootCauseMessage), OTel (traceId, spanId),
  circuit breaker (circuitBreakerState, fallbackTriggered), drops
  erroneous splitDepth/loopDepth columns
- Add all new fields through full ingestion/storage/API chain

UI:
- Fix overlay wrapper filtering: check wrapper type before status filter
- Add new fields to schema.d.ts

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 21:44:54 +01:00
hsiegeln
f12f5f3c8d feat: color minimap nodes by execution overlay state
All checks were successful
CI / cleanup-branch (push) Has been skipped
CI / build (push) Successful in 57s
CI / docker (push) Successful in 53s
CI / deploy-feature (push) Has been skipped
CI / deploy (push) Successful in 36s
Minimap reflects execution overlay: green for completed, red for failed,
grey for skipped nodes. ENDPOINT nodes are always green when overlay is
active (route entry point, same as main diagram logic).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 19:52:12 +01:00
hsiegeln
c6f70968a2 fix: update tests for new ProcessorRecord fields
All checks were successful
CI / cleanup-branch (push) Has been skipped
CI / build (push) Successful in 1m0s
CI / docker (push) Successful in 54s
CI / deploy-feature (push) Has been skipped
CI / deploy (push) Successful in 36s
Add resolvedEndpointUri, splitDepth, loopDepth arguments to
ProcessorRecord constructors in TreeReconstructionTest and
PostgresExecutionStoreIT.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 19:05:29 +01:00
hsiegeln
faf5d505f4 feat: support iteration wrapper nodes and filter overlay by selected iteration
Some checks failed
CI / cleanup-branch (push) Has been skipped
CI / build (push) Failing after 38s
CI / docker (push) Has been skipped
CI / deploy (push) Has been skipped
CI / deploy-feature (push) Has been skipped
Server:
- Add split_depth and loop_depth columns (V9 migration)
- Persist splitDepth/loopDepth with reflection fallback for older agent versions

UI:
- Detect iterations via wrapper processorTypes (loopIteration, splitIteration, multicastBranch)
- Filter overlay by selected iteration at the wrapper level
- Skip non-selected iteration wrappers entirely (wrapper + children)
- Don't add synthetic wrappers to overlay (no diagram node correspondence)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 18:57:27 +01:00
hsiegeln
c4b396e618 feat: persist and expose resolvedEndpointUri for execution-level drill-down
Wire resolvedEndpointUri through the full chain:
- V9 migration adds resolved_endpoint_uri column
- IngestionService extracts from ProcessorExecution
- PostgresExecutionStore persists and reads the column
- ProcessorNode includes field in detail API response
- UI schema updated for ProcessorNode and PositionedNode

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 18:37:11 +01:00
hsiegeln
e5e6175aca feat: use endpointUri for cross-route drill-down instead of label parsing
All checks were successful
CI / cleanup-branch (push) Has been skipped
CI / build (push) Successful in 1m2s
CI / docker (push) Successful in 1m0s
CI / deploy-feature (push) Has been skipped
CI / deploy (push) Successful in 38s
Server:
- Add endpointUri to PositionedNode (from RouteNode)
- Add fromEndpointUri to RouteSummary (catalog API)
- Catalog controller resolves endpoint URI from diagram store

UI:
- Build endpointRouteMap from catalog's fromEndpointUri field
- Drill-down uses exact match on node.endpointUri against the map
- Remove label parsing heuristics (extractTargetEndpoint, camelToKebab)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 18:31:08 +01:00
hsiegeln
0516207e83 fix: vertically center DO_TRY block relative to outer flow nodes
All checks were successful
CI / cleanup-branch (push) Has been skipped
CI / build (push) Successful in 1m0s
CI / docker (push) Successful in 51s
CI / deploy-feature (push) Has been skipped
CI / deploy (push) Successful in 37s
Remove PORT_ALIGNMENT_DEFAULT=BEGIN so NETWORK_SIMPLEX centers edges
at the vertical midpoint of the compound instead of the top.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 17:47:34 +01:00
hsiegeln
d79e7d0168 fix: color edges into compound nodes green when descendants were executed
Some checks failed
CI / cleanup-branch (push) Has been skipped
CI / build (push) Successful in 1m11s
CI / deploy (push) Has been cancelled
CI / deploy-feature (push) Has been cancelled
CI / docker (push) Has been cancelled
Edges into/out of compound nodes (DO_TRY, EIP_CHOICE, etc.) now show as
traversed (green) when any descendant node was executed, instead of grey.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 17:45:30 +01:00
hsiegeln
7c88b03956 fix: left-align DO_TRY sections and shrink container to fit content
All checks were successful
CI / cleanup-branch (push) Has been skipped
CI / build (push) Successful in 1m22s
CI / docker (push) Successful in 42s
CI / deploy-feature (push) Has been skipped
CI / deploy (push) Successful in 41s
- Left-align all sections (try_body, doFinally, doCatch) within DO_TRY
- Shrink DO_TRY height to match actual content, removing bottom padding

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 17:36:01 +01:00
hsiegeln
55e1c7cbb5 fix: improve DO_TRY diagram layout and node text clipping
All checks were successful
CI / cleanup-branch (push) Has been skipped
CI / build (push) Successful in 1m5s
CI / docker (push) Successful in 1m14s
CI / deploy (push) Successful in 49s
CI / deploy-feature (push) Has been skipped
- Use NETWORK_SIMPLEX placement for vertical centering of root flow nodes
- Skip structural edges from all compound nodes to descendants (not just DO_TRY)
- Reduce DO_TRY section spacing from NODE_SPACING*0.4 to fixed 20px
- Use SVG clipPath for node text instead of character-count truncation

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 17:28:07 +01:00
hsiegeln
6a1d199da6 fix: detect ARM64 architecture for sonar-scanner download
All checks were successful
CI / cleanup-branch (push) Has been skipped
CI / build (push) Successful in 1m20s
CI / docker (push) Successful in 13s
CI / deploy-feature (push) Has been skipped
CI / deploy (push) Successful in 35s
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 17:22:03 +01:00
hsiegeln
459f4d2e0c fix: improve diagram node readability and add UI to SonarQube scan
All checks were successful
CI / build (push) Successful in 1m8s
CI / cleanup-branch (push) Has been skipped
CI / docker (push) Successful in 1m0s
CI / deploy (push) Successful in 41s
CI / deploy-feature (push) Has been skipped
- Increase node width (160→220), height (40→50), spacing (90→120)
- Use SVG clipPath for text instead of character-count truncation
- Add UI sources, ESLint report, and sonar-scanner CLI to SonarQube workflow

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 17:16:36 +01:00
hsiegeln
27249c2440 feat: upgrade design system to v0.1.19, use onNavigate/fillHeight, add SonarQube workflow
All checks were successful
CI / build (push) Successful in 1m36s
CI / cleanup-branch (push) Has been skipped
CI / docker (push) Successful in 2m10s
CI / deploy (push) Successful in 50s
CI / deploy-feature (push) Has been skipped
- Use Sidebar onNavigate callback instead of display:contents click interception
- Use DataTable fillHeight prop instead of manual scroll wrapper divs
- Fix DataTable scroll/pagination by adding overflow:hidden to content container
- Fix left panel in split view to use flex column instead of overflow:auto
- Make error tab stack trace scrollable for large traces
- Add nightly SonarQube workflow with manual trigger support

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 16:57:12 +01:00
hsiegeln
f59423bc91 docs: add design system update instructions for Sidebar onNavigate and DataTable fillHeight
All checks were successful
CI / cleanup-branch (push) Has been skipped
CI / build (push) Successful in 1m8s
CI / docker (push) Successful in 1m1s
CI / deploy-feature (push) Has been skipped
CI / deploy (push) Successful in 35s
2026-03-28 16:20:45 +01:00
hsiegeln
e5be9f81e0 fix(ui): restore agents in sidebar for ops quick access
Some checks failed
CI / cleanup-branch (push) Has been skipped
CI / docker (push) Has been cancelled
CI / deploy (push) Has been cancelled
CI / deploy-feature (push) Has been cancelled
CI / build (push) Has been cancelled
2026-03-28 16:20:07 +01:00
hsiegeln
9f281c3354 chore(ui): remove dead code from navigation redesign
All checks were successful
CI / cleanup-branch (push) Has been skipped
CI / build (push) Successful in 1m3s
CI / docker (push) Successful in 1m2s
CI / deploy-feature (push) Has been skipped
CI / deploy (push) Successful in 37s
Deleted:
- ScopeTrail component (replaced by inline breadcrumb in TopBar)
- ExchangeList component (replaced by Dashboard DataTable)
- ExchangeDetail page (replaced by inline split view)

Removed from Dashboard:
- flattenProcessors() function (unused after detail panel removal)
- 11 dead CSS classes (panelSection, overviewGrid, errorBlock,
  inspectLink, openDetailLink, filterBar, etc.)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 16:14:15 +01:00
hsiegeln
f2a094f349 fix(ui): position config badges fully above node to avoid overlap
All checks were successful
CI / cleanup-branch (push) Has been skipped
CI / build (push) Successful in 1m4s
CI / docker (push) Successful in 55s
CI / deploy-feature (push) Has been skipped
CI / deploy (push) Successful in 36s
2026-03-28 16:06:34 +01:00
hsiegeln
dd1cae6f70 feat(ui): replace text badges with droplet/footprint icons matching context menu
Some checks failed
CI / cleanup-branch (push) Has been skipped
CI / docker (push) Has been cancelled
CI / deploy (push) Has been cancelled
CI / deploy-feature (push) Has been cancelled
CI / build (push) Has been cancelled
2026-03-28 16:05:45 +01:00
hsiegeln
7903a300db fix(ui): restore TRACE/TAP badges on diagram nodes via nodeConfigs
Some checks failed
CI / cleanup-branch (push) Has been skipped
CI / docker (push) Has been cancelled
CI / deploy (push) Has been cancelled
CI / deploy-feature (push) Has been cancelled
CI / build (push) Has been cancelled
2026-03-28 16:04:53 +01:00
hsiegeln
5873e6a57c fix(ui): keep execution overlay active when drilled down into sub-routes
Some checks failed
CI / cleanup-branch (push) Has been skipped
CI / build (push) Successful in 1m5s
CI / docker (push) Successful in 59s
CI / deploy-feature (push) Has been skipped
CI / deploy (push) Has been cancelled
2026-03-28 16:02:44 +01:00
hsiegeln
816a034d4a feat(ui): show process diagram when route is selected in sidebar
All checks were successful
CI / cleanup-branch (push) Has been skipped
CI / build (push) Successful in 1m47s
CI / docker (push) Successful in 1m3s
CI / deploy-feature (push) Has been skipped
CI / deploy (push) Successful in 37s
2026-03-28 15:58:38 +01:00
hsiegeln
2fade7192a fix(ui): prevent text selection on double-click in process diagram
Some checks failed
CI / build (push) Successful in 1m45s
CI / cleanup-branch (push) Has been skipped
CI / deploy (push) Has been cancelled
CI / deploy-feature (push) Has been cancelled
CI / docker (push) Has been cancelled
2026-03-28 15:56:36 +01:00
hsiegeln
175e62f514 docs: update navigation redesign spec to reflect final implementation
All checks were successful
CI / cleanup-branch (push) Has been skipped
CI / build (push) Successful in 1m3s
CI / docker (push) Successful in 8s
CI / deploy-feature (push) Has been skipped
CI / deploy (push) Successful in 35s
2026-03-28 15:51:37 +01:00
hsiegeln
b4c9be9334 feat(ui): browser Back/Forward restores exchange selection via history state
All checks were successful
CI / cleanup-branch (push) Has been skipped
CI / build (push) Successful in 1m1s
CI / docker (push) Successful in 57s
CI / deploy-feature (push) Has been skipped
CI / deploy (push) Successful in 36s
Each exchange selection (from table or correlation chain) pushes a
browser history entry with the selected exchange in location.state.
When the user navigates away (to agent details, app scope, etc.) and
presses Back, the previous history entry is restored and the split
view with the diagram reappears exactly as they left it.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 15:48:38 +01:00
hsiegeln
8b276a92a7 fix(ui): clicking app or route in exchange header clears selection and returns to table
All checks were successful
CI / cleanup-branch (push) Has been skipped
CI / build (push) Successful in 1m2s
CI / docker (push) Successful in 56s
CI / deploy-feature (push) Has been skipped
CI / deploy (push) Successful in 37s
2026-03-28 15:42:45 +01:00
hsiegeln
01c6d5c131 fix(ui): consistent attribute badge colors based on value hash across all views
All checks were successful
CI / cleanup-branch (push) Has been skipped
CI / build (push) Successful in 59s
CI / docker (push) Successful in 55s
CI / deploy-feature (push) Has been skipped
CI / deploy (push) Successful in 35s
2026-03-28 15:37:49 +01:00
hsiegeln
626501cb04 feat(ui): add Log tab to diagram detail panel with exchange/processor filtering
All checks were successful
CI / cleanup-branch (push) Has been skipped
CI / build (push) Successful in 1m1s
CI / docker (push) Successful in 56s
CI / deploy-feature (push) Has been skipped
CI / deploy (push) Successful in 38s
2026-03-28 15:32:55 +01:00
hsiegeln
3362417907 fix(ui): remove Duration label from correlation row, keep value only
Some checks failed
CI / cleanup-branch (push) Has been skipped
CI / build (push) Successful in 1m1s
CI / docker (push) Successful in 54s
CI / deploy-feature (push) Has been skipped
CI / deploy (push) Has been cancelled
2026-03-28 15:30:22 +01:00
hsiegeln
7b2622fca9 fix(ui): move correlation duration to far right
Some checks failed
CI / cleanup-branch (push) Has been skipped
CI / docker (push) Has been cancelled
CI / deploy (push) Has been cancelled
CI / deploy-feature (push) Has been cancelled
CI / build (push) Has been cancelled
2026-03-28 15:29:35 +01:00
hsiegeln
24d760af8a feat(ui): show total correlation duration (oldest start to latest end)
Some checks failed
CI / cleanup-branch (push) Has been skipped
CI / docker (push) Has been cancelled
CI / deploy (push) Has been cancelled
CI / deploy-feature (push) Has been cancelled
CI / build (push) Has been cancelled
2026-03-28 15:28:51 +01:00
hsiegeln
d32bde58e2 fix(ui): correlated exchange click updates local state instead of navigating
All checks were successful
CI / cleanup-branch (push) Has been skipped
CI / build (push) Successful in 55s
CI / docker (push) Successful in 53s
CI / deploy-feature (push) Has been skipped
CI / deploy (push) Successful in 37s
2026-03-28 15:26:01 +01:00
hsiegeln
3d86d57a80 fix(ui): always show correlation section, display message when none found
Some checks failed
CI / cleanup-branch (push) Has been skipped
CI / build (push) Successful in 1m0s
CI / docker (push) Has started running
CI / deploy (push) Has been cancelled
CI / deploy-feature (push) Has been cancelled
2026-03-28 15:24:20 +01:00
hsiegeln
29f4be542b fix(ui): exchange selection uses state, not URL navigation
All checks were successful
CI / cleanup-branch (push) Has been skipped
CI / build (push) Successful in 1m0s
CI / docker (push) Successful in 55s
CI / deploy-feature (push) Has been skipped
CI / deploy (push) Successful in 37s
Row click no longer navigates to /exchanges/:app/:route/:id which was
changing the search scope. Instead, Dashboard calls onExchangeSelect
callback and ExchangesPage manages the selected exchange as local state.
The search criteria and scope are preserved when selecting an exchange.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 15:20:17 +01:00
hsiegeln
2f2e503447 feat(ui): split agent links (app→overview, id→detail), color server icon by state
All checks were successful
CI / cleanup-branch (push) Has been skipped
CI / build (push) Successful in 1m2s
CI / docker (push) Successful in 52s
CI / deploy-feature (push) Has been skipped
CI / deploy (push) Successful in 36s
2026-03-28 15:16:54 +01:00
hsiegeln
7ee57ca975 feat(ui): make app/route/agent clickable in exchange header for navigation
Some checks failed
CI / cleanup-branch (push) Has been skipped
CI / build (push) Successful in 1m2s
CI / docker (push) Successful in 54s
CI / deploy-feature (push) Has been skipped
CI / deploy (push) Has been cancelled
2026-03-28 15:14:48 +01:00
hsiegeln
c8fcee9d09 feat(ui): add route and agent icons in exchange header
Some checks failed
CI / cleanup-branch (push) Has been skipped
CI / build (push) Successful in 58s
CI / docker (push) Successful in 52s
CI / deploy-feature (push) Has been skipped
CI / deploy (push) Has been cancelled
2026-03-28 15:12:37 +01:00
hsiegeln
0ed30d92f1 fix(ui): use application name as agent name in exchange header
Some checks failed
CI / cleanup-branch (push) Has been skipped
CI / build (push) Successful in 1m1s
CI / deploy (push) Has been cancelled
CI / deploy-feature (push) Has been cancelled
CI / docker (push) Has been cancelled
2026-03-28 15:11:23 +01:00
hsiegeln
4e59b0bcd0 fix(ui): remove exchange ID, reorder to app/route, add agent label
Some checks failed
CI / cleanup-branch (push) Has been skipped
CI / docker (push) Has been cancelled
CI / deploy (push) Has been cancelled
CI / deploy-feature (push) Has been cancelled
CI / build (push) Has been cancelled
2026-03-28 15:10:56 +01:00
hsiegeln
eaeef6f0b2 fix(ui): move agent ID before duration in exchange header
Some checks failed
CI / cleanup-branch (push) Has been skipped
CI / build (push) Successful in 58s
CI / docker (push) Successful in 52s
CI / deploy-feature (push) Has been cancelled
CI / deploy (push) Has been cancelled
2026-03-28 15:09:00 +01:00
hsiegeln
9f0c2e1225 feat(ui): show agent ID in exchange header info row
Some checks failed
CI / cleanup-branch (push) Has been skipped
CI / build (push) Successful in 1m0s
CI / docker (push) Has been cancelled
CI / deploy (push) Has been cancelled
CI / deploy-feature (push) Has been cancelled
2026-03-28 15:07:58 +01:00
hsiegeln
e934b31164 feat(ui): show tap-collected attributes as badges in exchange header
Some checks failed
CI / cleanup-branch (push) Has been skipped
CI / docker (push) Has been cancelled
CI / deploy (push) Has been cancelled
CI / deploy-feature (push) Has been cancelled
CI / build (push) Has been cancelled
2026-03-28 15:07:23 +01:00
hsiegeln
77d871c4f8 fix(ui): sort headers alphabetically in diagram detail panel
Some checks failed
CI / cleanup-branch (push) Has been skipped
CI / build (push) Successful in 1m1s
CI / docker (push) Has been cancelled
CI / deploy (push) Has been cancelled
CI / deploy-feature (push) Has been cancelled
2026-03-28 15:05:47 +01:00
hsiegeln
4296d41cad fix(ui): show full exchange ID without truncation
All checks were successful
CI / cleanup-branch (push) Has been skipped
CI / build (push) Successful in 1m1s
CI / docker (push) Successful in 52s
CI / deploy-feature (push) Has been skipped
CI / deploy (push) Successful in 37s
2026-03-28 15:02:52 +01:00
hsiegeln
a5ba684c7d feat(ui): redesign ExchangeHeader with info bar, arrows, and navigation
Some checks failed
CI / cleanup-branch (push) Has been skipped
CI / build (push) Successful in 1m3s
CI / docker (push) Successful in 56s
CI / deploy-feature (push) Has been skipped
CI / deploy (push) Has been cancelled
- Always shows exchange info row: status dot, badge, ID, route, app, duration
- Correlation chain: arrow connectors between nodes, route name + duration per node
- Click on correlated exchange navigates to /exchanges/:app/:route/:exchangeId
- Compact styling with bg-raised background, proper visual hierarchy
- Horizontal scroll for long correlation chains

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 15:00:45 +01:00
hsiegeln
a658ed9135 Revert "fix(ui): pin DataTable pagination to bottom, table body scrolls independently"
All checks were successful
CI / cleanup-branch (push) Has been skipped
CI / build (push) Successful in 58s
CI / docker (push) Successful in 9s
CI / deploy-feature (push) Has been skipped
CI / deploy (push) Successful in 35s
This reverts commit b863370511.
2026-03-28 14:57:16 +01:00
hsiegeln
b863370511 fix(ui): pin DataTable pagination to bottom, table body scrolls independently
Some checks failed
CI / cleanup-branch (push) Has been skipped
CI / docker (push) Has been cancelled
CI / deploy (push) Has been cancelled
CI / deploy-feature (push) Has been cancelled
CI / build (push) Has been cancelled
2026-03-28 14:56:23 +01:00
hsiegeln
048f6566a9 fix(ui): make exchange table fill page height with vertical scroll
Some checks failed
CI / cleanup-branch (push) Has been skipped
CI / build (push) Successful in 58s
CI / docker (push) Successful in 50s
CI / deploy-feature (push) Has been skipped
CI / deploy (push) Has been cancelled
2026-03-28 14:54:29 +01:00
hsiegeln
5cb3de03af fix(ui): remove whitespace between components for integrated layout
Some checks failed
CI / cleanup-branch (push) Has been skipped
CI / build (push) Successful in 1m1s
CI / deploy (push) Has been cancelled
CI / deploy-feature (push) Has been cancelled
CI / docker (push) Has been cancelled
2026-03-28 14:52:53 +01:00
hsiegeln
ef9d8c8066 fix(ui): remove summary section from ExchangeHeader, keep only correlation chain
Some checks failed
CI / cleanup-branch (push) Has been skipped
CI / build (push) Successful in 59s
CI / docker (push) Successful in 53s
CI / deploy-feature (push) Has been skipped
CI / deploy (push) Has been cancelled
2026-03-28 14:50:42 +01:00
hsiegeln
1ca4cac396 fix(ui): restore proper correlation chain styling with StatusDot, route names, colored borders
Some checks failed
CI / cleanup-branch (push) Has been skipped
CI / docker (push) Has been cancelled
CI / deploy (push) Has been cancelled
CI / deploy-feature (push) Has been cancelled
CI / build (push) Has been cancelled
2026-03-28 14:49:45 +01:00
hsiegeln
6b06e7f86b fix(ui): remove shortcuts bar from Dashboard
Some checks failed
CI / cleanup-branch (push) Has been skipped
CI / build (push) Successful in 1m0s
CI / docker (push) Has been cancelled
CI / deploy (push) Has been cancelled
CI / deploy-feature (push) Has been cancelled
2026-03-28 14:47:57 +01:00
hsiegeln
e703a9d39d fix(ui): remove exchange summary bar from ExecutionDiagram
Some checks failed
CI / cleanup-branch (push) Has been skipped
CI / docker (push) Has been cancelled
CI / deploy (push) Has been cancelled
CI / deploy-feature (push) Has been cancelled
CI / build (push) Has been cancelled
2026-03-28 14:47:03 +01:00
hsiegeln
67bae5640c refactor(ui): remove KPI strip from Dashboard — metrics now in tab bar
Some checks failed
CI / cleanup-branch (push) Has been skipped
CI / build (push) Successful in 1m2s
CI / deploy (push) Has been cancelled
CI / deploy-feature (push) Has been cancelled
CI / docker (push) Has been cancelled
2026-03-28 14:45:08 +01:00
hsiegeln
c06f0c89e5 feat(ui): add compact KPI metrics in tab bar (Total, Err%, Avg, P99)
Some checks failed
CI / cleanup-branch (push) Has been skipped
CI / build (push) Successful in 1m1s
CI / docker (push) Successful in 51s
CI / deploy-feature (push) Has been skipped
CI / deploy (push) Has been cancelled
New TabKpis component shows scope-aware metrics with trend arrows
aligned right in the content tab bar. Each metric shows current value
and an arrow indicating change vs previous period (green=good, red=bad).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 14:42:58 +01:00
hsiegeln
73560d761d fix(ui): pass onNodeAction to diagram components to restore context menu
All checks were successful
CI / cleanup-branch (push) Has been skipped
CI / build (push) Successful in 1m10s
CI / docker (push) Successful in 1m1s
CI / deploy-feature (push) Has been skipped
CI / deploy (push) Successful in 37s
2026-03-28 14:37:58 +01:00
hsiegeln
4ed804141a fix(ui): add top offset to diagram reset view to clear breadcrumb bar
Some checks failed
CI / cleanup-branch (push) Has been skipped
CI / build (push) Successful in 1m10s
CI / deploy (push) Has been cancelled
CI / deploy-feature (push) Has been cancelled
CI / docker (push) Has been cancelled
2026-03-28 14:36:24 +01:00
hsiegeln
de2281cad2 fix(ui): move minimap above zoom controls in bottom-right corner
Some checks failed
CI / cleanup-branch (push) Has been skipped
CI / build (push) Successful in 1m7s
CI / deploy (push) Has been cancelled
CI / deploy-feature (push) Has been cancelled
CI / docker (push) Has been cancelled
2026-03-28 14:35:04 +01:00
hsiegeln
5af20d0f63 refactor(ui): remove detail panel slide-in and inspect column from exchange table
Some checks failed
CI / cleanup-branch (push) Has been skipped
CI / build (push) Successful in 1m12s
CI / docker (push) Successful in 1m5s
CI / deploy-feature (push) Has been skipped
CI / deploy (push) Has been cancelled
Row click now navigates directly to the split view with diagram.
Removed: DetailPanel, inspect column, unused imports (ExternalLink,
ProcessorTimeline, RouteFlow, useExecutionDetail, useDiagramLayout,
buildFlowSegments).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 14:32:20 +01:00
hsiegeln
91171590e6 feat(ui): add draggable splitter between search results and diagram panel
All checks were successful
CI / cleanup-branch (push) Has been skipped
CI / build (push) Successful in 1m9s
CI / docker (push) Successful in 1m1s
CI / deploy-feature (push) Has been skipped
CI / deploy (push) Successful in 37s
2026-03-28 14:29:19 +01:00
hsiegeln
699ef86f8f fix(ui): use Tabs instead of SegmentedTabs for content navigation
Some checks failed
CI / cleanup-branch (push) Has been skipped
CI / build (push) Successful in 1m4s
CI / docker (push) Has started running
CI / deploy (push) Has been cancelled
CI / deploy-feature (push) Has been cancelled
2026-03-28 14:27:28 +01:00
hsiegeln
d63a9f8ce7 fix(ui): move scope trail into TopBar breadcrumb instead of separate element
Some checks failed
CI / cleanup-branch (push) Has been skipped
CI / docker (push) Has been cancelled
CI / deploy (push) Has been cancelled
CI / deploy-feature (push) Has been cancelled
CI / build (push) Has been cancelled
2026-03-28 14:26:36 +01:00
hsiegeln
77c73fe3e6 fix(ui): use display:contents on sidebar wrapper to preserve flex layout
Some checks failed
CI / cleanup-branch (push) Has been skipped
CI / build (push) Successful in 1m2s
CI / deploy (push) Has been cancelled
CI / docker (push) Has been cancelled
CI / deploy-feature (push) Has been cancelled
2026-03-28 14:25:20 +01:00
hsiegeln
1e6de17084 fix(ui): restore layout — same table everywhere, 50:50 split, full-height sidebar, tab styling
All checks were successful
CI / cleanup-branch (push) Has been skipped
CI / build (push) Successful in 1m2s
CI / docker (push) Successful in 59s
CI / deploy-feature (push) Has been skipped
CI / deploy (push) Successful in 36s
- Sidebar wrapper gets height:100% to fill window
- Route-scoped Exchanges uses same Dashboard table (not compact ExchangeList)
- 50:50 grid split: table on left, diagram on right when route selected
- ContentTabs gets border-bottom and surface background for visibility

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 14:22:34 +01:00
7ee7076eec Merge pull request 'feat/navigation-redesign' (#92) from feat/navigation-redesign into main
All checks were successful
CI / cleanup-branch (push) Has been skipped
CI / build (push) Successful in 1m0s
CI / docker (push) Successful in 9s
CI / deploy-feature (push) Has been skipped
CI / deploy (push) Successful in 35s
Reviewed-on: cameleer/cameleer3-server#92
2026-03-28 14:09:38 +01:00
hsiegeln
698b97d536 fix(ui): update Dashboard links to use new exchange URL structure
All checks were successful
CI / cleanup-branch (push) Has been skipped
CI / build (push) Successful in 1m5s
CI / docker (push) Successful in 54s
CI / deploy (push) Has been skipped
CI / deploy-feature (push) Successful in 40s
CI / cleanup-branch (pull_request) Has been skipped
CI / build (pull_request) Successful in 1m8s
CI / docker (pull_request) Has been skipped
CI / deploy (pull_request) Has been skipped
CI / deploy-feature (pull_request) Has been skipped
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-28 14:02:49 +01:00
hsiegeln
4fe418cc89 feat(ui): integrate ContentTabs, ScopeTrail, and sidebar scope interception
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 14:01:52 +01:00
hsiegeln
66abb1fe3a feat(ui): restructure router for tab-based navigation with legacy redirects
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-28 13:59:20 +01:00
hsiegeln
611c201887 feat(ui): add RuntimePage and DashboardPage tab wrappers
Thin wrapper pages that conditionally render AgentHealth/AgentInstance
and RoutesMetrics/RouteDetail based on URL params for the nav redesign.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-28 13:58:10 +01:00
hsiegeln
f2abe296ee feat(ui): add ExchangesPage with full-width and 3-column modes
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-28 13:57:13 +01:00
hsiegeln
fc27880d96 feat(ui): add ExchangeHeader component with correlation chain
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-28 13:55:13 +01:00
hsiegeln
8219c54422 feat(ui): add ExchangeList compact component for 3-column layout
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-28 13:55:08 +01:00
hsiegeln
c1b156bdb4 feat(ui): add ContentTabs component (Exchanges | Dashboard | Runtime)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-28 13:52:54 +01:00
hsiegeln
0eb377b515 feat(ui): add ScopeTrail component for scope-based breadcrumbs
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-28 13:52:49 +01:00
hsiegeln
facf7fb6ef feat(ui): add useScope hook for tab+scope URL management 2026-03-28 13:51:35 +01:00
hsiegeln
90be1875e0 refactor: simplify ElkDiagramRenderer layout code
All checks were successful
CI / cleanup-branch (push) Has been skipped
CI / build (push) Successful in 59s
CI / docker (push) Successful in 40s
CI / deploy-feature (push) Has been skipped
CI / deploy (push) Successful in 36s
- Introduce LayoutContext to bundle 8 accumulator params into 1 object
- Extract computeLayout (261 lines) into 6 focused sub-methods:
  buildNodeIndex, partitionNodes, createElkRoot, createElkEdges,
  postProcessDoTrySections, extractLayout
- Consolidate duplicated DO_TRY handler iteration via orderedHandlerChildren
- De-duplicate ELK root configuration (main + handler roots)
- Add DO_TRY test cases for section ordering and uniform width
- Clean up orphaned Javadoc comments

No behavioral changes. 882 → 841 lines.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 12:20:08 +01:00
hsiegeln
065517f032 fix: align main flow at DO_TRY top and stretch sections to full width
All checks were successful
CI / cleanup-branch (push) Has been skipped
CI / build (push) Successful in 59s
CI / docker (push) Successful in 41s
CI / deploy-feature (push) Has been skipped
CI / deploy (push) Successful in 36s
Port alignment BEGIN on DO_TRY compounds makes edges attach at the top
instead of center, keeping the main flow level. Post-processing also
stretches all DO_TRY sections (doFinally, doCatch) to match the widest
section's width for visual consistency.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 10:26:28 +01:00
hsiegeln
99b97c53dd fix: restore node click/dblclick by limiting pointer capture to empty space
All checks were successful
CI / cleanup-branch (push) Has been skipped
CI / build (push) Successful in 59s
CI / docker (push) Successful in 54s
CI / deploy-feature (push) Has been skipped
CI / deploy (push) Successful in 35s
setPointerCapture on the SVG redirected click/dblclick events away from
node <g> elements, breaking drill-down (double-click) and potentially
click selection. Now only capture the pointer when clicking on empty SVG
space, preserving normal event flow on nodes while keeping drag-to-pan.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 10:11:47 +01:00
hsiegeln
79e5caaf7a fix: post-process ELK graph to enforce DO_TRY section order
All checks were successful
CI / cleanup-branch (push) Has been skipped
CI / build (push) Successful in 1m2s
CI / docker (push) Successful in 1m1s
CI / deploy-feature (push) Has been skipped
CI / deploy (push) Successful in 37s
ELK's partitioning doesn't reliably order disconnected children within
a compound node. Instead, let ELK lay out freely then re-stack sections
in correct order (try_body → doFinally → doCatch) by adjusting Y
positions in the ELK graph before extraction. This propagates correctly
to both node and edge coordinates via getAbsoluteY().

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 10:03:50 +01:00
hsiegeln
5b5fa28ba0 fix: use ELK partitioning to enforce DO_TRY section order
All checks were successful
CI / cleanup-branch (push) Has been skipped
CI / build (push) Successful in 57s
CI / docker (push) Successful in 41s
CI / deploy-feature (push) Has been skipped
CI / deploy (push) Successful in 37s
Invisible ordering edges caused horizontal layout. Replace with ELK's
partitioning feature which explicitly assigns sections to ordered layers:
try_body (partition 0) → doFinally (1) → doCatch (2).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 09:44:16 +01:00
hsiegeln
3b2c5ccdbe fix: use invisible ordering edges to enforce DO_TRY section order
All checks were successful
CI / cleanup-branch (push) Has been skipped
CI / build (push) Successful in 1m1s
CI / docker (push) Successful in 39s
CI / deploy-feature (push) Has been skipped
CI / deploy (push) Successful in 36s
Layer constraints (FIRST/LAST) don't work for disconnected components
in ELK's layered algorithm. Replace with invisible edges that chain
try_body → doFinally → doCatch to guarantee correct top-to-bottom order.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 09:37:49 +01:00
hsiegeln
c8d824d347 fix: only skip DO_TRY edges to internal children, keep continuation edges
All checks were successful
CI / cleanup-branch (push) Has been skipped
CI / build (push) Successful in 1m1s
CI / docker (push) Successful in 40s
CI / deploy-feature (push) Has been skipped
CI / deploy (push) Successful in 36s
The previous fix skipped ALL edges from DO_TRY nodes, which also
removed the continuation edge to the next node in the main flow
(causing LOG nodes to appear disconnected). Now checks if the target
is a descendant of the DO_TRY ELK node — only internal edges are
skipped, continuation edges to the next main flow node are kept.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 09:18:26 +01:00
hsiegeln
615a3c6e99 fix: order DO_TRY sections as try-body, finally, catch and reduce spacing
All checks were successful
CI / cleanup-branch (push) Has been skipped
CI / build (push) Successful in 59s
CI / docker (push) Successful in 38s
CI / deploy-feature (push) Has been skipped
CI / deploy (push) Successful in 36s
ELK TB layout places children in insertion order. Now explicitly adds
DO_FINALLY before DO_CATCH so the visual order inside DO_TRY is:
try body (top) → finally → catch blocks (bottom). Also reduces
internal spacing to keep the compound more compact.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 09:15:40 +01:00
hsiegeln
dbf64ecb48 feat: render doTry/doCatch/doFinally like route-level handler sections
All checks were successful
CI / cleanup-branch (push) Has been skipped
CI / build (push) Successful in 1m3s
CI / docker (push) Successful in 55s
CI / deploy-feature (push) Has been skipped
CI / deploy (push) Successful in 38s
Backend: DO_TRY compounds now use a virtual _TRY_BODY wrapper with LR
layout for the try body, while DO_CATCH/DO_FINALLY stack below as
separate sections (TB). Edges from DO_TRY are skipped like route-level
handler edges. Removes ELK-v2 debug logging.

Frontend: _TRY_BODY renders as transparent wrapper, DO_CATCH as red
tinted section, DO_FINALLY as teal section. DO_FINALLY color changed
from red to teal (completion handler, not error).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 09:04:36 +01:00
hsiegeln
1702200a60 feat: Cmd+K Enter applies full-text search to dashboard
All checks were successful
CI / cleanup-branch (push) Has been skipped
CI / build (push) Successful in 1m47s
CI / docker (push) Successful in 1m16s
CI / deploy-feature (push) Has been skipped
CI / deploy (push) Successful in 36s
When pressing Enter in the command palette without explicitly selecting
a result (via arrow keys or mouse), the search query is now applied as
a server-side full-text filter on the Dashboard table. Explicit
selection still navigates to the exchange. Updates design system to
v0.1.18 for the new onSubmit prop.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 23:33:39 +01:00
hsiegeln
004574d442 fix: allow drag-to-pan over diagram nodes and compounds
All checks were successful
CI / cleanup-branch (push) Has been skipped
CI / build (push) Successful in 58s
CI / docker (push) Successful in 55s
CI / deploy-feature (push) Has been skipped
CI / deploy (push) Successful in 37s
Previously onPointerDown bailed out when the target was inside a node
(data-node-id), blocking pan entirely over nodes and compound groups.
Now panning always starts, and a didPan ref distinguishes drag from
click — node click handlers skip selection when the user was dragging.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 23:21:05 +01:00
hsiegeln
41111b082c chore: replace Unicode/emoji icons with Lucide React
All checks were successful
CI / cleanup-branch (push) Has been skipped
CI / build (push) Successful in 1m14s
CI / docker (push) Successful in 1m11s
CI / deploy-feature (push) Has been skipped
CI / deploy (push) Successful in 37s
Adds lucide-react and replaces all HTML entity and emoji icons across
the UI with proper SVG icon components. Tree-shaken — only imported
icons are bundled.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 23:16:39 +01:00
hsiegeln
e9b1c94d1a fix: move status filtering server-side in Dashboard search
All checks were successful
CI / cleanup-branch (push) Has been skipped
CI / build (push) Successful in 59s
CI / docker (push) Successful in 54s
CI / deploy-feature (push) Has been skipped
CI / deploy (push) Successful in 35s
The Dashboard was fetching 50 results without a status filter and
filtering client-side, causing fewer matches when filtering by error
compared to route-specific pages that filter server-side. Now passes
statusFilters to the OpenSearch query. Backend supports comma-separated
status values for multi-select filters.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 23:00:32 +01:00
hsiegeln
0d7d04501c chore: resize minimap to match zoom controls width (140x90)
All checks were successful
CI / cleanup-branch (push) Has been skipped
CI / build (push) Successful in 58s
CI / docker (push) Successful in 54s
CI / deploy-feature (push) Has been skipped
CI / deploy (push) Successful in 37s
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 22:41:06 +01:00
hsiegeln
6393e5096f chore: move minimap to top-right corner of diagram
Some checks failed
CI / cleanup-branch (push) Has been skipped
CI / build (push) Has started running
CI / docker (push) Has been cancelled
CI / deploy (push) Has been cancelled
CI / deploy-feature (push) Has been cancelled
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 22:40:28 +01:00
hsiegeln
4af71aabac fix: use graph root + edge walk to separate main flow from handlers
Some checks failed
CI / cleanup-branch (push) Has been skipped
CI / build (push) Successful in 58s
CI / docker (push) Has been cancelled
CI / deploy (push) Has been cancelled
CI / deploy-feature (push) Has been cancelled
Root cause: graph.getNodes() is a flat list with duplicates — handler
compound children appear both nested inside their parent AND as
top-level entries. The previous separation tried to filter the flat
list but missed the duplicates, leaving handler children in rootNode.

New approach: walk from graph.getRoot() following non-ERROR edges to
discover main flow nodes. Edges targeting handler compounds (ON_EXCEPTION,
ON_COMPLETION) are not followed. This cleanly separates main flow from
handler sections using the graph's own structure.

Falls back to flat list filtering (old behavior) when graph.getRoot()
is null (legacy/test graphs).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 22:39:04 +01:00
hsiegeln
acb7cade90 fix: exclude handler compound children from main flow ELK graph
All checks were successful
CI / cleanup-branch (push) Has been skipped
CI / build (push) Successful in 56s
CI / docker (push) Successful in 39s
CI / deploy-feature (push) Has been skipped
CI / deploy (push) Successful in 35s
Root cause found: RouteGraph.getNodes() is a FLAT list that includes
handler compound children (log8, setBody1, etc.) as top-level entries
alongside the main flow nodes. The handler separation only identified
the compound PARENTS (ON_EXCEPTION) but not their children, so 7
handler children leaked into rootNode as main flow nodes, causing
ELK to place the real main flow at wrong Y positions.

Fix: two-pass separation — first identify handler compounds and
collect ALL descendant IDs, then build mainNodes excluding both
handler compounds AND their descendants.

Debug logging left in temporarily for verification.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 22:33:40 +01:00
hsiegeln
19d3c8fa93 debug: v2 ELK logging to verify handler separation in new build
All checks were successful
CI / cleanup-branch (push) Has been skipped
CI / build (push) Successful in 57s
CI / docker (push) Successful in 38s
CI / deploy-feature (push) Has been skipped
CI / deploy (push) Successful in 35s
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 22:29:23 +01:00
hsiegeln
990d607d4b fix: normalize main flow section to (0,0) origin in frontend
All checks were successful
CI / cleanup-branch (push) Has been skipped
CI / build (push) Successful in 58s
CI / docker (push) Successful in 49s
CI / deploy-feature (push) Has been skipped
CI / deploy (push) Successful in 36s
The root cause of the Y-offset: ELK places main flow nodes at
arbitrary positions (e.g., y=679) within its root graph, and the
frontend rendered them at those raw positions. Handler sections were
already normalized via shiftNodes, but the main section was not.

Now useDiagramData.ts applies the same normalization to the main
section: computes bounding box, shifts nodes and edges so the section
starts at (0,0). This fixes the Y-offset regardless of what ELK
produces internally.

Removed the backend normalizePositions (was ineffective because handler
nodes at y=12 dominated the global minimum, preventing meaningful shift
of main flow nodes at y=679).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 22:26:35 +01:00
hsiegeln
0df7735d20 fix: comprehensive ElkDiagramRenderer cleanup and Y-offset fix
All checks were successful
CI / cleanup-branch (push) Has been skipped
CI / build (push) Successful in 55s
CI / docker (push) Successful in 38s
CI / deploy-feature (push) Has been skipped
CI / deploy (push) Successful in 35s
Based on thorough code review, fixes all identified issues:

1. **Y-offset root cause**: Added post-layout normalization that shifts
   all positioned nodes and edges so the bounding box starts at (0,0).
   ELK can place nodes at arbitrary positions within its root graph;
   normalizing compensates regardless of what ELK computes internally.

2. **Bounding box**: Compute from recursively flattened node tree +
   edge point bounds. Removes double-counting of compound children
   (children have absolute coords, not relative to parent).

3. **SVG double-drawing**: Compound children were drawn both inside
   drawCompoundContainer and again in the allNodes loop. Now collects
   compound child IDs and skips them in the second pass.

4. **findNode**: Now recurses into children for nested compound lookup.

5. **colorForType**: Removed redundant double-check on EIP_TYPES.

6. **Dead code removed**: routeNodeMap/indexNodeRecursive (populated but
   never read), MIN_NODE_WIDTH/CHAR_WIDTH/LABEL_PADDING (unused).

7. **Static initialization**: LayoutMetaDataProvider registration moved
   from constructor to static block (runs once, not per instance).

8. **Debug logging removed**: Removed diagnostic System.out.println.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 22:20:33 +01:00
hsiegeln
7926179ed9 debug: add ELK root layout logging to diagnose Y-offset issue
All checks were successful
CI / cleanup-branch (push) Has been skipped
CI / build (push) Successful in 57s
CI / docker (push) Successful in 38s
CI / deploy-feature (push) Has been skipped
CI / deploy (push) Successful in 34s
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 22:14:16 +01:00
hsiegeln
1855153dbe fix: proper LCA and bounding box for multi-root ELK layout
All checks were successful
CI / cleanup-branch (push) Has been skipped
CI / build (push) Successful in 58s
CI / docker (push) Successful in 38s
CI / deploy-feature (push) Has been skipped
CI / deploy (push) Successful in 35s
1. findCommonParent: replaced with correct lowest common ancestor
   algorithm using ancestor set intersection (previous version only
   walked from node 'a', not a true LCA)

2. Bounding box: compute totalWidth/totalHeight from actual positioned
   node coordinates instead of rootNode.getWidth/Height. The rootNode
   dimensions don't account for handler sections in separate ELK roots.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 22:09:16 +01:00
hsiegeln
3751762c69 fix: use correct ELK root for handler node coordinate extraction
All checks were successful
CI / cleanup-branch (push) Has been skipped
CI / build (push) Successful in 58s
CI / docker (push) Successful in 39s
CI / deploy-feature (push) Has been skipped
CI / deploy (push) Successful in 36s
Handler section nodes were positioned relative to rootNode, but they
live under separate handlerRoot ELK graphs. Using getElkRoot() to find
each node's actual root ensures correct absolute coordinates.

This combined with the POLYLINE edge routing should eliminate the
Y-offset misalignment between main flow nodes.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 21:53:36 +01:00
hsiegeln
56f98671ca fix: straight edge routing and handler section edge extraction
All checks were successful
CI / cleanup-branch (push) Has been skipped
CI / build (push) Successful in 58s
CI / docker (push) Successful in 53s
CI / deploy-feature (push) Has been skipped
CI / deploy (push) Successful in 37s
Backend:
- Set POLYLINE edge routing on ELK root — eliminates curved/bent edges
  between horizontally aligned nodes
- Collect edges from handler section roots (not just main root) so
  internal handler edges are included in the layout output
- Use correct root reference for coordinate calculation per edge

Frontend:
- Render ALL edge points as line segments (polylines), not cubic bezier.
  ELK bend points are waypoints, not bezier control points — the cubic
  bezier interpretation caused false curves.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 20:59:38 +01:00
hsiegeln
cbe41d7ac7 feat: configure-tap action navigates to AppConfig page
All checks were successful
CI / cleanup-branch (push) Has been skipped
CI / build (push) Successful in 57s
CI / docker (push) Successful in 52s
CI / deploy-feature (push) Has been skipped
CI / deploy (push) Successful in 35s
The tap button in the node toolbar now navigates to
/admin/appconfig?app=<application>&processor=<nodeId>, which
auto-selects the application in the AppConfigPage. The AppConfigPage
reads the ?app query param to open the detail panel for that app.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 20:23:19 +01:00
hsiegeln
bd8e95c6ce fix: add HIERARCHY_HANDLING to handler section ELK roots
All checks were successful
CI / cleanup-branch (push) Has been skipped
CI / build (push) Successful in 57s
CI / docker (push) Successful in 38s
CI / deploy-feature (push) Has been skipped
CI / deploy (push) Successful in 37s
Handler section ELK roots were missing INCLUDE_CHILDREN, causing
edges between a handler compound and its children to fail with
UnsupportedGraphException (cross-hierarchy edge resolution).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 20:15:36 +01:00
hsiegeln
fee9b4bd83 fix: skip edges that cross ELK root graph boundaries
All checks were successful
CI / cleanup-branch (push) Has been skipped
CI / build (push) Successful in 59s
CI / docker (push) Successful in 38s
CI / deploy-feature (push) Has been skipped
CI / deploy (push) Successful in 38s
Edges connecting main flow nodes to handler section nodes (ON_EXCEPTION,
ON_COMPLETION) now span different ELK root graphs. ELK throws
UnsupportedGraphException when an edge connects nodes in different
layout hierarchies. Skip these cross-root edges — the frontend doesn't
render them anyway (handler sections are separated visually).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 20:11:25 +01:00
hsiegeln
7ec683aca0 chore: replace toolbar icons — footprints for trace, tap for tap config
Some checks failed
CI / cleanup-branch (push) Has been skipped
CI / build (push) Successful in 59s
CI / docker (push) Successful in 51s
CI / deploy-feature (push) Has been skipped
CI / deploy (push) Has been cancelled
- Toggle tracing: "T" → 👣 (footprints — trace = following the path)
- Configure tap: ✎ (pencil) → 🚰 (water tap — tap = intercept the flow)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 20:09:12 +01:00
hsiegeln
ac750b603f fix: enable scrollbar on detail panel tab content
All checks were successful
CI / cleanup-branch (push) Has been skipped
CI / build (push) Successful in 59s
CI / docker (push) Successful in 51s
CI / deploy-feature (push) Has been skipped
CI / deploy (push) Successful in 36s
The flex chain from detailArea → detailPanel → tabContent lacked
min-height: 0, so flex children never shrank below content height
and overflow-y: auto never triggered. Added min-height: 0 and
flex: 1 to propagate the height constraint correctly.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 20:05:48 +01:00
hsiegeln
5306be3f2e fix: lay out handler sections in separate ELK graphs
All checks were successful
CI / cleanup-branch (push) Has been skipped
CI / build (push) Successful in 58s
CI / docker (push) Successful in 37s
CI / deploy-feature (push) Has been skipped
CI / deploy (push) Successful in 36s
ON_EXCEPTION, ON_COMPLETION, and ERROR_HANDLER compounds were included
in the same root ELK graph as the main flow. ELK's layered algorithm
offset the main flow nodes vertically to accommodate the handler
compounds, causing bent arrows between the ENDPOINT and first processor.

Now handler sections get their own independent ELK root graphs. The
frontend already separates and repositions them, so they just need
correct internal layout — not positioning relative to the main flow.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 20:03:20 +01:00
hsiegeln
b0dcd0ac6b fix: update test ProcessorRecord constructors for iteration fields
All checks were successful
CI / cleanup-branch (push) Has been skipped
CI / build (push) Successful in 55s
CI / docker (push) Successful in 51s
CI / deploy-feature (push) Has been skipped
CI / deploy (push) Successful in 36s
Tests were using the old 18-param constructor, missing the 5 new
iteration fields (loopIndex, loopSize, splitIndex, splitSize,
multicastIndex) added in V8 migration.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 19:56:54 +01:00
hsiegeln
159e4adf07 chore: remove /dev/diagram test page
Some checks failed
CI / cleanup-branch (push) Has been skipped
CI / build (push) Failing after 32s
CI / docker (push) Has been skipped
CI / deploy (push) Has been skipped
CI / deploy-feature (push) Has been skipped
No longer needed — the ProcessDiagram is now integrated into
ExchangeDetail via the ExecutionDiagram wrapper.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 19:55:07 +01:00
hsiegeln
085c4e395b feat: execution overlay & debugger (sub-project 2)
Some checks failed
CI / cleanup-branch (push) Has been skipped
CI / build (push) Failing after 36s
CI / docker (push) Has been skipped
CI / deploy (push) Has been skipped
CI / deploy-feature (push) Has been skipped
Adds execution overlay to the ProcessDiagram component, turning it into
an after-the-fact debugger for Camel route executions.

Backend:
- Flyway V8: iteration fields (loop/split/multicast index/size) on processor_executions
- Snapshot-by-processorId endpoint for robust processor lookup
- ELK LINEAR_SEGMENTS node placement for consistent Y-alignment

Frontend:
- ExecutionDiagram wrapper: exchange bar, resizable splitter, detail panel
- Node overlay: green tint+checkmark (completed), red tint+! (failed), dimmed (skipped)
- Edge overlay: green solid (traversed), dashed gray (not traversed)
- Per-compound iteration stepper for loops/splits/multicasts
- 7-tab detail panel: Info, Headers, Input, Output, Error, Config, Timeline
- Jump to Error: selects + centers viewport on failed processor
- Triggered error handler sections highlighted with solid red frame
- Drill-down disables overlay (sub-routes show topology only)
- Integrated into ExchangeDetail page Flow view

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 19:51:55 +01:00
hsiegeln
d7166b6d0a feat: Jump to Error centers the failed node in the viewport
Added centerOnNodeId prop to ProcessDiagram. When set, the diagram
pans to center the specified node in the viewport. Jump to Error
now selects the failed processor AND centers the viewport on it.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 19:51:00 +01:00
hsiegeln
25e23c0b87 feat: highlight triggered error handler sections
When an onException/error handler section has any executed processors
(overlay entries), it renders with a stronger red tint (8% vs 3%),
a solid red border frame, and a solid divider line. This makes it
easy to identify which handler was triggered when multiple exist.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 19:47:57 +01:00
hsiegeln
cf9e847f84 fix: use design system CodeBlock for error stack trace
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 19:45:54 +01:00
hsiegeln
bfd76261ef fix: disable execution overlay when drilled into sub-route
The execution overlay data maps to the root route's processor IDs. When
drilled into a sub-route, those IDs don't match, causing all nodes to
appear dimmed. Now clears the overlay and shows pure topology when
viewing a sub-route via drill-down.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 19:43:51 +01:00
hsiegeln
0b8efa1998 fix: drill-down uses route-based fetch instead of pre-loaded layout
When drilled into a sub-route, the pre-fetched diagramLayout (loaded by
content hash for the root execution) doesn't contain the sub-route's
diagram. Only use the pre-loaded layout for the root route; fall back to
useDiagramByRoute for drilled-down sub-routes.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 19:40:20 +01:00
hsiegeln
3027e9b24f fix: scrollable headers/timeline, CodeBlock for body, ELK node alignment
- Make headers tab and timeline tab scrollable when content overflows
- Replace custom <pre> code block with design system CodeBlock component
  for body tabs (Input/Output) to match existing styleguide
- Add LINEAR_SEGMENTS node placement strategy to ELK layout to fix
  Y-offset misalignment between nodes in left-to-right diagrams
  (e.g., ENDPOINT at different Y level than subsequent processors)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 19:34:25 +01:00
hsiegeln
3d5d462de0 fix: ENDPOINT node execution state, badge position, and edge traversal
- Synthesize COMPLETED state for ENDPOINT nodes when overlay is active
  (endpoints are route entry points, not in the processor execution tree)
- Move status badge (check/error) inside the card (top-right, below top bar)
  to avoid collision with ConfigBadge (TRACE/TAP) badges
- Include ENDPOINT nodes in edge traversal check so the edge from
  endpoint to first processor renders as green/traversed

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 19:29:30 +01:00
hsiegeln
f675451384 fix: use non-passive wheel listener to prevent page scroll during diagram zoom
React's onWheel is passive by default, so preventDefault() doesn't stop
page scrolling. Attach native wheel listener with { passive: false } via
useEffect instead.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 19:24:09 +01:00
hsiegeln
021a52e56b feat: integrate ExecutionDiagram into ExchangeDetail flow view
Replace the RouteFlow-based flow view with the new ExecutionDiagram
component which provides execution overlay, iteration stepping, and
an integrated detail panel. The gantt view and all other page sections
remain unchanged.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 19:12:11 +01:00
hsiegeln
5ccefa3cdb feat: add ExecutionDiagram wrapper component
Composes ProcessDiagram with execution overlay data, exchange summary
bar, resizable splitter, and detail panel into a single root component.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 19:05:43 +01:00
hsiegeln
e4c66b1311 feat: add DetailPanel with 7 tabs for execution diagram overlay
Implements the bottom detail panel with processor header bar, tab bar
(Info, Headers, Input, Output, Error, Config, Timeline), and all tab
content components. Info shows processor/exchange metadata in a grid,
Headers fetches per-processor snapshots for side-by-side display,
Input/Output render formatted code blocks, Error extracts exception
types, Config is a placeholder, and Timeline renders a Gantt chart.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 19:01:53 +01:00
hsiegeln
5da03d0938 feat: add useExecutionOverlay and useIterationState hooks
useExecutionOverlay maps processor tree to overlay state map, handling
iteration filtering, sub-route failure detection, and trace data flags.
useIterationState detects compound nodes with iterated children and
manages per-compound iteration selection.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 18:56:38 +01:00
hsiegeln
3af1d1f3b6 feat: add useProcessorSnapshotById hook for snapshot-by-processorId endpoint
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 18:54:01 +01:00
hsiegeln
1984c597de feat: add iteration stepper to compound nodes and thread overlay props
Add a left/right stepper widget to compound node headers (LOOP, SPLIT,
MULTICAST) when iteration overlay data is present. Thread executionOverlay,
overlayActive, iterationState, and onIterationChange props through
ProcessDiagram -> CompoundNode -> children and ProcessDiagram ->
ErrorSection -> children so leaf DiagramNode instances render with
execution state (green/red badges, dimming for skipped nodes).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 18:52:32 +01:00
hsiegeln
3029704051 feat: add traversed/not-traversed visual states to DiagramEdge
Add green solid edges for traversed paths and dashed gray for
not-traversed when execution overlay is active. Includes green
arrowhead marker and overlay threading through CompoundNode and
ErrorSection.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 18:47:59 +01:00
hsiegeln
2b805ec196 feat: add execution overlay visual states to DiagramNode
DiagramNode now accepts executionState and overlayActive props to render
execution status: green tint + checkmark badge for completed nodes, red
tint + exclamation badge for failed nodes, dimmed opacity for skipped
nodes. Duration is shown at bottom-right, and a drill-down arrow appears
for sub-route failures.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 18:44:16 +01:00
hsiegeln
ff59dc5d57 feat: add execution overlay types and extend ProcessDiagram with diagramLayout prop
Define the execution overlay type system (NodeExecutionState, IterationInfo,
DetailTab) and extend ProcessDiagramProps with optional overlay props. Add
diagramLayout prop so ExecutionDiagram can pass a pre-fetched layout by content
hash, bypassing the internal route-based fetch in useDiagramData.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 18:40:57 +01:00
hsiegeln
3928743ea7 feat: update OpenAPI spec and TypeScript types for execution overlay
Add iteration fields (loopIndex, loopSize, splitIndex, splitSize,
multicastIndex) to ProcessorNode schema. Add new endpoint path
/executions/{executionId}/processors/by-id/{processorId}/snapshot.
Remove stale diagramNodeId field that was dropped in V6 migration.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 18:38:09 +01:00
hsiegeln
cf6c4bd60c feat: add snapshot-by-processorId endpoint for robust processor lookup
Add GET /executions/{id}/processors/by-id/{processorId}/snapshot endpoint
that fetches processor snapshot data by processorId instead of positional
index, which is fragile when the tree structure changes. The existing
index-based endpoint remains unchanged for backward compatibility.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 18:34:45 +01:00
hsiegeln
edd841ffeb feat: add iteration fields to processor execution storage
Add loop_index, loop_size, split_index, split_size, multicast_index
columns to processor_executions table and thread them through the
full storage → ingestion → detail pipeline. These fields enable
execution overlay to display iteration context for loop, split,
and multicast EIPs.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 18:32:47 +01:00
hsiegeln
889f0e5263 chore: add .worktrees/ to .gitignore for worktree isolation 2026-03-27 18:27:34 +01:00
hsiegeln
3a41e1f1d3 docs: add execution overlay implementation plan (sub-project 2)
12 tasks covering backend prerequisites (iteration fields, snapshot-by-id
endpoint), ProcessDiagram overlay props, node/edge visual states, compound
iteration stepper, detail panel with 7 tabs, ExecutionDiagram wrapper,
and ExchangeDetail page integration.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 18:25:47 +01:00
hsiegeln
509159417b docs: add execution overlay & debugger design spec (sub-project 2)
Design for overlaying real execution data onto the ProcessDiagram:
- Node status visualization (green OK, red failed, dimmed skipped)
- Per-compound iteration stepping for loops/splits
- Tabbed detail panel (Info, Headers, Input, Output, Error, Config, Timeline)
- Jump to Error with cross-route drill-down
- Backend prerequisites for iteration fields and snapshot-by-id endpoint

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 18:13:03 +01:00
hsiegeln
30c8fe1091 feat: add minimap overview to process diagram
All checks were successful
CI / cleanup-branch (push) Has been skipped
CI / build (push) Successful in 1m0s
CI / docker (push) Successful in 57s
CI / deploy-feature (push) Has been skipped
CI / deploy (push) Successful in 37s
Small overview panel in the bottom-left showing the full diagram
layout with colored node rectangles and an amber viewport indicator.
Click or drag on the minimap to pan the main diagram.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 17:16:05 +01:00
hsiegeln
b1ff05439a docs: update design spec and increase section gap to 80px
All checks were successful
CI / cleanup-branch (push) Has been skipped
CI / build (push) Successful in 1m5s
CI / docker (push) Successful in 54s
CI / deploy-feature (push) Has been skipped
CI / deploy (push) Successful in 35s
Update design spec with implementation notes covering recursive
compound nesting, edge z-ordering, ON_COMPLETION sections, drill-down
navigation, CSS transform zoom, and HTML overlay toolbar.

Increase SECTION_GAP to 80px for better visual separation between
completion and error handler sections.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 17:10:01 +01:00
202 changed files with 20058 additions and 8326 deletions

View File

@@ -0,0 +1,89 @@
name: SonarQube
on:
schedule:
- cron: '0 2 * * *'
workflow_dispatch:
jobs:
sonarqube:
runs-on: ubuntu-latest
container:
image: gitea.siegeln.net/cameleer/cameleer-build:1
credentials:
username: cameleer
password: ${{ secrets.REGISTRY_TOKEN }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Configure Gitea Maven Registry
run: |
mkdir -p ~/.m2
cat > ~/.m2/settings.xml << 'SETTINGS'
<settings>
<servers>
<server>
<id>gitea</id>
<username>cameleer</username>
<password>${env.REGISTRY_TOKEN}</password>
</server>
</servers>
</settings>
SETTINGS
env:
REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }}
- name: Cache Maven dependencies
uses: actions/cache@v4
with:
path: ~/.m2/repository
key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }}
restore-keys: ${{ runner.os }}-maven-
- name: Build and Test Java
run: mvn clean verify -DskipITs -U --batch-mode
- name: Install UI dependencies
working-directory: ui
run: |
echo '//gitea.siegeln.net/api/packages/cameleer/npm/:_authToken=${REGISTRY_TOKEN}' >> .npmrc
npm ci
env:
REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }}
- name: Lint UI
working-directory: ui
run: npm run lint -- --format json --output-file eslint-report.json || true
- name: Install sonar-scanner
run: |
SONAR_SCANNER_VERSION=6.2.1.4610
ARCH=$(uname -m)
case "$ARCH" in
aarch64|arm64) PLATFORM="linux-aarch64" ;;
*) PLATFORM="linux-x64" ;;
esac
curl -sSLo sonar-scanner.zip "https://binaries.sonarsource.com/Distribution/sonar-scanner-cli/sonar-scanner-cli-${SONAR_SCANNER_VERSION}-${PLATFORM}.zip"
unzip -q sonar-scanner.zip
ln -s "$(pwd)/sonar-scanner-${SONAR_SCANNER_VERSION}-${PLATFORM}/bin/sonar-scanner" /usr/local/bin/sonar-scanner
- name: SonarQube Analysis
run: |
sonar-scanner \
-Dsonar.host.url="$SONAR_HOST_URL" \
-Dsonar.token="$SONAR_TOKEN" \
-Dsonar.projectKey=cameleer3-server \
-Dsonar.projectName="Cameleer3 Server" \
-Dsonar.sources=cameleer3-server-core/src/main/java,cameleer3-server-app/src/main/java,ui/src \
-Dsonar.tests=cameleer3-server-core/src/test/java,cameleer3-server-app/src/test/java \
-Dsonar.java.binaries=cameleer3-server-core/target/classes,cameleer3-server-app/target/classes \
-Dsonar.java.test.binaries=cameleer3-server-core/target/test-classes,cameleer3-server-app/target/test-classes \
-Dsonar.java.libraries="$HOME/.m2/repository/**/*.jar" \
-Dsonar.typescript.eslint.reportPaths=ui/eslint-report.json \
-Dsonar.eslint.reportPaths=ui/eslint-report.json \
-Dsonar.exclusions="ui/node_modules/**,ui/dist/**,**/target/**"
env:
SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }}
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}

1
.gitignore vendored
View File

@@ -39,3 +39,4 @@ logs/
# Claude
.claude/
.worktrees/

Binary file not shown.

After

Width:  |  Height:  |  Size: 142 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 141 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 115 KiB

View File

@@ -0,0 +1 @@
{"reason":"idle timeout","timestamp":1774616238650}

View File

@@ -0,0 +1 @@
10188

View File

@@ -0,0 +1,105 @@
<h2>ProcessDiagram Component Hierarchy</h2>
<p class="subtitle">How the SVG rendering is structured — from data fetch to pixels</p>
<div class="section">
<div class="mockup">
<div class="mockup-header">Component Tree</div>
<div class="mockup-body" style="padding: 20px; font-family: 'JetBrains Mono', monospace; font-size: 13px; line-height: 1.8; color: #1A1612;">
<div><strong style="color: #1A7F8E;">ProcessDiagram</strong> &mdash; root, fetches layout, manages state</div>
<div style="padding-left: 24px; border-left: 2px solid #E4DFD8;">
<div>&lt;svg&gt; container with viewBox (zoom/pan transforms)</div>
<div style="padding-left: 24px; border-left: 2px solid #E4DFD8;">
<div><strong style="color: #7C3AED;">DiagramSection</strong> label="Main Route"</div>
<div style="padding-left: 24px; border-left: 2px solid #E4DFD8;">
<div><strong style="color: #9C9184;">&lt;g&gt;</strong> edges layer (rendered first, behind nodes)</div>
<div style="padding-left: 24px;">
<div><strong style="color: #C6820E;">DiagramEdge</strong> &times; N &mdash; SVG &lt;path&gt; with arrowhead</div>
</div>
<div><strong style="color: #9C9184;">&lt;g&gt;</strong> nodes layer</div>
<div style="padding-left: 24px;">
<div><strong style="color: #C6820E;">DiagramNode</strong> &times; N &mdash; top-bar card</div>
<div style="padding-left: 24px; border-left: 2px solid #E4DFD8;">
<div><strong style="color: #3D7C47;">ConfigBadge</strong> &times; 0..N &mdash; tap/trace indicators</div>
<div><strong style="color: #3D7C47;">NodeToolbar</strong> &mdash; floating on hover</div>
</div>
<div><strong style="color: #C6820E;">CompoundNode</strong> &times; 0..N &mdash; choice/split container</div>
<div style="padding-left: 24px; border-left: 2px solid #E4DFD8;">
<div><strong style="color: #C6820E;">DiagramNode</strong> &times; N &mdash; children inside compound</div>
</div>
</div>
</div>
<div style="margin-top: 8px;"><strong style="color: #C0392B;">DiagramSection</strong> label="onException" variant="error"</div>
<div style="padding-left: 24px; border-left: 2px solid #C0392B;">
<div><em style="color: #9C9184;">same edge + node structure as above</em></div>
</div>
</div>
<div style="margin-top: 8px;"><strong style="color: #1A7F8E;">ZoomControls</strong> &mdash; HTML overlay (not SVG)</div>
</div>
</div>
</div>
</div>
<div class="section" style="margin-top: 24px;">
<div class="mockup">
<div class="mockup-header">SVG Structure (simplified)</div>
<div class="mockup-body" style="padding: 20px; font-family: 'JetBrains Mono', monospace; font-size: 12px; line-height: 1.7; color: #5C5347; background: #F5F2ED;">
<pre style="margin: 0;">&lt;div class="process-diagram"&gt; <span style="color:#9C9184">/* wrapper div */</span>
&lt;svg viewBox="0 0 {w} {h}"&gt; <span style="color:#9C9184">/* zoom = viewBox transform */</span>
&lt;g class="diagram-content"&gt; <span style="color:#9C9184">/* pan offset */</span>
<span style="color:#7C3AED">&lt;!-- Main Route section --&gt;</span>
&lt;g class="section section--main"&gt;
&lt;g class="edges"&gt;
&lt;path d="M 100 40 C ..." /&gt; <span style="color:#9C9184">/* cubic bezier edge */</span>
&lt;marker&gt;...&lt;/marker&gt; <span style="color:#9C9184">/* arrowhead def */</span>
&lt;/g&gt;
&lt;g class="nodes"&gt;
&lt;g transform="translate(x, y)"&gt; <span style="color:#9C9184">/* positioned by ELK */</span>
&lt;rect .../&gt; <span style="color:#9C9184">/* card background */</span>
&lt;rect .../&gt; <span style="color:#9C9184">/* color top bar */</span>
&lt;text&gt;LOG&lt;/text&gt; <span style="color:#9C9184">/* label */</span>
&lt;g class="badges"&gt;...&lt;/g&gt; <span style="color:#9C9184">/* config indicators */</span>
&lt;/g&gt;
&lt;/g&gt;
&lt;/g&gt;
<span style="color:#C0392B">&lt;!-- Error Handler section --&gt;</span>
&lt;g class="section section--error"
transform="translate(0, {mainH + gap})"&gt;
&lt;text&gt;onException&lt;/text&gt; <span style="color:#9C9184">/* section label */</span>
&lt;line .../&gt; <span style="color:#9C9184">/* divider line */</span>
&lt;g class="edges"&gt;...&lt;/g&gt;
&lt;g class="nodes"&gt;...&lt;/g&gt;
&lt;/g&gt;
&lt;/g&gt;
&lt;/svg&gt;
&lt;div class="zoom-controls"&gt;...&lt;/div&gt; <span style="color:#9C9184">/* HTML overlay */</span>
&lt;/div&gt;</pre>
</div>
</div>
</div>
<div class="section" style="margin-top: 24px;">
<div class="mockup">
<div class="mockup-header">Data Flow</div>
<div class="mockup-body" style="padding: 20px; font-family: 'JetBrains Mono', monospace; font-size: 12px; line-height: 1.8; color: #5C5347;">
<pre style="margin: 0;">
<span style="color:#1A7F8E">GET /diagrams/{hash}/render?direction=LR</span>
DiagramLayout { nodes[], edges[], width, height }
<span style="color:#7C3AED">separateFlows(nodes)</span> → mainNodes[] + errorSections[]
│ │
▼ ▼
<span style="color:#C6820E">renderMainSection()</span> <span style="color:#C0392B">renderErrorSection()</span>
│ │
▼ ▼
SVG groups with SVG groups offset below
ELK x/y coordinates main section by mainHeight + gap
</pre>
</div>
</div>
</div>

View File

@@ -0,0 +1,164 @@
<h2>Node Interactions & Config Badges</h2>
<p class="subtitle">Hover toolbar, selection states, and active config indicators</p>
<div class="section">
<div class="mockup">
<div class="mockup-header">Node States</div>
<div class="mockup-body" style="padding: 24px; background: #F5F2ED;">
<svg width="100%" height="340" viewBox="0 0 520 340">
<!-- 1. Normal state -->
<text x="10" y="16" fill="#9C9184" font-size="11" font-weight="600">NORMAL</text>
<g transform="translate(10, 24)">
<rect x="0" y="0" width="200" height="56" rx="4" fill="white" stroke="#E4DFD8" stroke-width="1"/>
<rect x="0" y="0" width="200" height="6" rx="4" fill="#C6820E"/>
<rect x="4" y="0" width="192" height="6" fill="#C6820E"/>
<text x="16" y="32" fill="#C6820E" font-size="14">&#9881;</text>
<text x="36" y="28" fill="#1A1612" font-size="11" font-weight="600">LOG</text>
<text x="36" y="44" fill="#5C5347" font-size="10">Processing order</text>
</g>
<!-- 2. Hovered state with toolbar -->
<text x="270" y="16" fill="#9C9184" font-size="11" font-weight="600">HOVERED (toolbar appears)</text>
<g transform="translate(270, 24)">
<rect x="0" y="0" width="200" height="56" rx="4" fill="#FFFCF5" stroke="#C6820E" stroke-width="1.5"/>
<rect x="0" y="0" width="200" height="6" rx="4" fill="#C6820E"/>
<rect x="4" y="0" width="192" height="6" fill="#C6820E"/>
<text x="16" y="32" fill="#C6820E" font-size="14">&#9881;</text>
<text x="36" y="28" fill="#1A1612" font-size="11" font-weight="600">LOG</text>
<text x="36" y="44" fill="#5C5347" font-size="10">Processing order</text>
<!-- Floating toolbar -->
<g transform="translate(30, -32)">
<rect x="0" y="0" width="140" height="28" rx="6" fill="#1A1612" opacity="0.92"/>
<!-- Icons as circles -->
<g transform="translate(10, 4)">
<circle cx="10" cy="10" r="9" fill="rgba(255,255,255,0.15)"/>
<text x="10" y="14" fill="white" font-size="11" text-anchor="middle">&#128269;</text>
</g>
<g transform="translate(40, 4)">
<circle cx="10" cy="10" r="9" fill="rgba(255,255,255,0.15)"/>
<text x="10" y="14" fill="white" font-size="11" text-anchor="middle">T</text>
</g>
<g transform="translate(70, 4)">
<circle cx="10" cy="10" r="9" fill="rgba(255,255,255,0.15)"/>
<text x="10" y="14" fill="white" font-size="11" text-anchor="middle">&#9998;</text>
</g>
<g transform="translate(100, 4)">
<circle cx="10" cy="10" r="9" fill="rgba(255,255,255,0.15)"/>
<text x="10" y="14" fill="white" font-size="11" text-anchor="middle">&#8943;</text>
</g>
</g>
</g>
<!-- 3. Selected state -->
<text x="10" y="112" fill="#9C9184" font-size="11" font-weight="600">SELECTED (click)</text>
<g transform="translate(10, 120)">
<rect x="-2" y="-2" width="204" height="60" rx="6" fill="none" stroke="#C6820E" stroke-width="2.5" stroke-dasharray="none"/>
<rect x="0" y="0" width="200" height="56" rx="4" fill="white" stroke="#C6820E" stroke-width="1"/>
<rect x="0" y="0" width="200" height="6" rx="4" fill="#C6820E"/>
<rect x="4" y="0" width="192" height="6" fill="#C6820E"/>
<text x="16" y="32" fill="#C6820E" font-size="14">&#9881;</text>
<text x="36" y="28" fill="#1A1612" font-size="11" font-weight="600">LOG</text>
<text x="36" y="44" fill="#5C5347" font-size="10">Processing order</text>
</g>
<!-- 4. With config badges -->
<text x="270" y="112" fill="#9C9184" font-size="11" font-weight="600">WITH CONFIG BADGES</text>
<g transform="translate(270, 120)">
<rect x="0" y="0" width="200" height="56" rx="4" fill="white" stroke="#E4DFD8" stroke-width="1"/>
<rect x="0" y="0" width="200" height="6" rx="4" fill="#C6820E"/>
<rect x="4" y="0" width="192" height="6" fill="#C6820E"/>
<text x="16" y="32" fill="#C6820E" font-size="14">&#9881;</text>
<text x="36" y="28" fill="#1A1612" font-size="11" font-weight="600">LOG</text>
<text x="36" y="44" fill="#5C5347" font-size="10">Processing order</text>
<!-- Trace badge (top-right corner) -->
<g transform="translate(165, -6)">
<rect x="0" y="0" width="38" height="16" rx="8" fill="#1A7F8E"/>
<text x="19" y="12" fill="white" font-size="8" font-weight="600" text-anchor="middle">TRACE</text>
</g>
<!-- Tap badge -->
<g transform="translate(124, -6)">
<rect x="0" y="0" width="36" height="16" rx="8" fill="#7C3AED"/>
<text x="18" y="12" fill="white" font-size="8" font-weight="600" text-anchor="middle">TAP</text>
</g>
</g>
<!-- 5. Error node style -->
<text x="10" y="210" fill="#9C9184" font-size="11" font-weight="600">ERROR HANDLER NODE</text>
<g transform="translate(10, 218)">
<rect x="0" y="0" width="200" height="56" rx="4" fill="white" stroke="#E4DFD8" stroke-width="1"/>
<rect x="0" y="0" width="200" height="6" rx="4" fill="#C0392B"/>
<rect x="4" y="0" width="192" height="6" fill="#C0392B"/>
<text x="16" y="32" fill="#C0392B" font-size="14">&#9888;</text>
<text x="36" y="28" fill="#1A1612" font-size="11" font-weight="600">ON_EXCEPTION</text>
<text x="36" y="44" fill="#5C5347" font-size="10">java.lang.Exception</text>
</g>
<!-- 6. Compound node (Choice) -->
<text x="270" y="210" fill="#9C9184" font-size="11" font-weight="600">COMPOUND NODE (CHOICE)</text>
<g transform="translate(270, 218)">
<rect x="0" y="0" width="220" height="110" rx="4" fill="white" stroke="#7C3AED" stroke-width="1.5"/>
<rect x="0" y="0" width="220" height="22" rx="4" fill="#7C3AED"/>
<rect x="4" y="4" width="212" height="18" fill="#7C3AED"/>
<text x="110" y="16" fill="white" font-size="10" font-weight="600" text-anchor="middle">&#9670; CHOICE</text>
<!-- Children -->
<g transform="translate(10, 30)">
<rect x="0" y="0" width="200" height="32" rx="3" fill="white" stroke="#E4DFD8" stroke-width="1"/>
<rect x="0" y="0" width="200" height="4" rx="3" fill="#7C3AED"/>
<rect x="3" y="0" width="194" height="4" fill="#7C3AED"/>
<text x="12" y="22" fill="#7C3AED" font-size="10">&#9670;</text>
<text x="28" y="22" fill="#1A1612" font-size="10" font-weight="600">WHEN</text>
<text x="66" y="22" fill="#5C5347" font-size="9">type == 'A'</text>
</g>
<g transform="translate(10, 70)">
<rect x="0" y="0" width="200" height="32" rx="3" fill="white" stroke="#E4DFD8" stroke-width="1"/>
<rect x="0" y="0" width="200" height="4" rx="3" fill="#7C3AED"/>
<rect x="3" y="0" width="194" height="4" fill="#7C3AED"/>
<text x="12" y="22" fill="#7C3AED" font-size="10">&#9670;</text>
<text x="28" y="22" fill="#1A1612" font-size="10" font-weight="600">OTHERWISE</text>
</g>
</g>
</svg>
</div>
</div>
</div>
<div class="section" style="margin-top: 24px;">
<div class="mockup">
<div class="mockup-header">Toolbar Actions</div>
<div class="mockup-body" style="padding: 16px;">
<table style="width: 100%; border-collapse: collapse; font-size: 13px;">
<thead>
<tr style="border-bottom: 2px solid #E4DFD8;">
<th style="text-align: left; padding: 8px; color: #5C5347;">Icon</th>
<th style="text-align: left; padding: 8px; color: #5C5347;">Action</th>
<th style="text-align: left; padding: 8px; color: #5C5347;">Description</th>
</tr>
</thead>
<tbody>
<tr style="border-bottom: 1px solid #EDE9E3;">
<td style="padding: 8px;">&#128269;</td>
<td style="padding: 8px; font-weight: 600;">Inspect</td>
<td style="padding: 8px; color: #5C5347;">Select node &amp; open detail side-panel</td>
</tr>
<tr style="border-bottom: 1px solid #EDE9E3;">
<td style="padding: 8px;">T</td>
<td style="padding: 8px; font-weight: 600;">Toggle Trace</td>
<td style="padding: 8px; color: #5C5347;">Enable/disable capture of input+output for this processor</td>
</tr>
<tr style="border-bottom: 1px solid #EDE9E3;">
<td style="padding: 8px;">&#9998;</td>
<td style="padding: 8px; font-weight: 600;">Configure Tap</td>
<td style="padding: 8px; color: #5C5347;">Open tap expression editor for this processor</td>
</tr>
<tr>
<td style="padding: 8px;">&#8943;</td>
<td style="padding: 8px; font-weight: 600;">More</td>
<td style="padding: 8px; color: #5C5347;">Copy processor ID, jump to code, view in search</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>

View File

@@ -0,0 +1,119 @@
<h2>Node Interaction Model</h2>
<p class="subtitle">What happens when you interact with a processor node on the diagram?</p>
<div class="cards">
<!-- Option A: Click-to-select + context menu -->
<div class="card" data-choice="a" onclick="toggleSelect(this)">
<div class="card-image" style="padding: 24px; background: #F5F2ED;">
<svg width="100%" height="180" viewBox="0 0 420 180">
<!-- Normal node -->
<g transform="translate(10, 10)">
<rect x="0" y="0" width="180" height="56" rx="4" fill="white" stroke="#E4DFD8" stroke-width="1"/>
<rect x="0" y="0" width="180" height="6" rx="4" fill="#C6820E"/>
<rect x="4" y="0" width="172" height="6" fill="#C6820E"/>
<text x="16" y="32" fill="#C6820E" font-size="14">&#9881;</text>
<text x="36" y="30" fill="#1A1612" font-size="11" font-weight="600">LOG</text>
<text x="36" y="44" fill="#5C5347" font-size="10">Processing order</text>
<text x="96" y="72" fill="#9C9184" font-size="10" text-anchor="middle" font-style="italic">normal state</text>
</g>
<!-- Selected node (amber ring) -->
<g transform="translate(10, 100)">
<rect x="-2" y="-2" width="184" height="60" rx="6" fill="none" stroke="#C6820E" stroke-width="2.5"/>
<rect x="0" y="0" width="180" height="56" rx="4" fill="white" stroke="#C6820E" stroke-width="1"/>
<rect x="0" y="0" width="180" height="6" rx="4" fill="#C6820E"/>
<rect x="4" y="0" width="172" height="6" fill="#C6820E"/>
<text x="16" y="32" fill="#C6820E" font-size="14">&#9881;</text>
<text x="36" y="30" fill="#1A1612" font-size="11" font-weight="600">LOG</text>
<text x="36" y="44" fill="#5C5347" font-size="10">Processing order</text>
<text x="96" y="72" fill="#C6820E" font-size="10" text-anchor="middle" font-weight="600">click = select</text>
</g>
<!-- Context menu on right-click -->
<g transform="translate(220, 10)">
<rect x="0" y="0" width="180" height="56" rx="4" fill="white" stroke="#E4DFD8" stroke-width="1"/>
<rect x="0" y="0" width="180" height="6" rx="4" fill="#C6820E"/>
<rect x="4" y="0" width="172" height="6" fill="#C6820E"/>
<text x="16" y="32" fill="#C6820E" font-size="14">&#9881;</text>
<text x="36" y="30" fill="#1A1612" font-size="11" font-weight="600">LOG</text>
<text x="36" y="44" fill="#5C5347" font-size="10">Processing order</text>
<!-- Context menu -->
<g transform="translate(100, 40)">
<rect x="0" y="0" width="140" height="96" rx="6" fill="white" stroke="#E4DFD8" stroke-width="1" filter="url(#shadow)"/>
<text x="12" y="20" fill="#1A1612" font-size="11">&#128269; View Snapshot</text>
<line x1="8" y1="28" x2="132" y2="28" stroke="#EDE9E3" stroke-width="1"/>
<text x="12" y="44" fill="#1A7F8E" font-size="11">&#9881; Enable Tracing</text>
<text x="12" y="64" fill="#1A7F8E" font-size="11">&#128204; Set Tap</text>
<line x1="8" y1="72" x2="132" y2="72" stroke="#EDE9E3" stroke-width="1"/>
<text x="12" y="88" fill="#5C5347" font-size="11">&#128203; Copy Processor ID</text>
</g>
<text x="90" y="152" fill="#9C9184" font-size="10" text-anchor="middle" font-style="italic">right-click = context menu</text>
</g>
<defs>
<filter id="shadow" x="-4" y="-2" width="148" height="104">
<feDropShadow dx="0" dy="2" stdDeviation="4" flood-opacity="0.12"/>
</filter>
</defs>
</svg>
</div>
<div class="card-body">
<h3>A: Click-Select + Right-Click Menu</h3>
<p>Click to select a node (amber highlight ring). Right-click for context menu with tracing/tap/snapshot actions. Clean separation of concerns. Standard desktop UX.</p>
</div>
</div>
<!-- Option B: Hover toolbar -->
<div class="card" data-choice="b" onclick="toggleSelect(this)">
<div class="card-image" style="padding: 24px; background: #F5F2ED;">
<svg width="100%" height="180" viewBox="0 0 420 180">
<!-- Normal node -->
<g transform="translate(10, 10)">
<rect x="0" y="0" width="180" height="56" rx="4" fill="white" stroke="#E4DFD8" stroke-width="1"/>
<rect x="0" y="0" width="180" height="6" rx="4" fill="#C6820E"/>
<rect x="4" y="0" width="172" height="6" fill="#C6820E"/>
<text x="16" y="32" fill="#C6820E" font-size="14">&#9881;</text>
<text x="36" y="30" fill="#1A1612" font-size="11" font-weight="600">LOG</text>
<text x="36" y="44" fill="#5C5347" font-size="10">Processing order</text>
<text x="96" y="72" fill="#9C9184" font-size="10" text-anchor="middle" font-style="italic">normal state</text>
</g>
<!-- Hovered node with floating toolbar -->
<g transform="translate(10, 100)">
<rect x="0" y="0" width="180" height="56" rx="4" fill="#FFFCF5" stroke="#C6820E" stroke-width="1.5"/>
<rect x="0" y="0" width="180" height="6" rx="4" fill="#C6820E"/>
<rect x="4" y="0" width="172" height="6" fill="#C6820E"/>
<text x="16" y="32" fill="#C6820E" font-size="14">&#9881;</text>
<text x="36" y="30" fill="#1A1612" font-size="11" font-weight="600">LOG</text>
<text x="36" y="44" fill="#5C5347" font-size="10">Processing order</text>
<!-- Floating toolbar above -->
<g transform="translate(20, -30)">
<rect x="0" y="0" width="140" height="26" rx="13" fill="#1A1612" opacity="0.9"/>
<text x="18" y="17" fill="white" font-size="12" title="View">&#128269;</text>
<text x="46" y="17" fill="white" font-size="12" title="Trace">&#9881;</text>
<text x="74" y="17" fill="white" font-size="12" title="Tap">&#128204;</text>
<text x="102" y="17" fill="white" font-size="12" title="Copy">&#128203;</text>
<text x="124" y="17" fill="white" font-size="12" title="More">&#8943;</text>
</g>
<text x="96" y="72" fill="#C6820E" font-size="10" text-anchor="middle" font-weight="600">hover = toolbar appears</text>
</g>
<!-- Click = select (same as A) -->
<g transform="translate(220, 50)">
<rect x="-2" y="-2" width="184" height="60" rx="6" fill="none" stroke="#C6820E" stroke-width="2.5"/>
<rect x="0" y="0" width="180" height="56" rx="4" fill="white" stroke="#C6820E" stroke-width="1"/>
<rect x="0" y="0" width="180" height="6" rx="4" fill="#C6820E"/>
<rect x="4" y="0" width="172" height="6" fill="#C6820E"/>
<text x="16" y="32" fill="#C6820E" font-size="14">&#9881;</text>
<text x="36" y="30" fill="#1A1612" font-size="11" font-weight="600">LOG</text>
<text x="36" y="44" fill="#5C5347" font-size="10">Processing order</text>
<text x="96" y="72" fill="#C6820E" font-size="10" text-anchor="middle" font-weight="600">click = select</text>
</g>
</svg>
</div>
<div class="card-body">
<h3>B: Hover Floating Toolbar</h3>
<p>Hover reveals a dark floating icon toolbar above the node. Click still selects. More discoverable than right-click, but can feel cluttered on dense diagrams.</p>
</div>
</div>
</div>

View File

@@ -0,0 +1,208 @@
<h2>Node Visual Style</h2>
<p class="subtitle">Which processor node style fits our design system best? Think MuleSoft / TIBCO BW5 but adapted to our warm parchment theme.</p>
<div class="cards">
<!-- Option A: Icon-first blocks (MuleSoft-inspired) -->
<div class="card" data-choice="a" onclick="toggleSelect(this)">
<div class="card-image" style="padding: 24px; background: #F5F2ED;">
<svg width="100%" height="220" viewBox="0 0 400 220">
<!-- FROM node -->
<g transform="translate(20, 10)">
<rect x="0" y="0" width="160" height="56" rx="8" fill="#1A7F8E" opacity="0.12" stroke="#1A7F8E" stroke-width="1.5"/>
<rect x="0" y="0" width="42" height="56" rx="8" fill="#1A7F8E"/>
<rect x="8" y="0" width="34" height="56" fill="#1A7F8E"/>
<text x="21" y="34" fill="white" font-size="20" text-anchor="middle">&#9654;</text>
<text x="100" y="25" fill="#1A1612" font-size="12" font-weight="600" text-anchor="middle">FROM</text>
<text x="100" y="42" fill="#5C5347" font-size="11" text-anchor="middle">direct:orders</text>
</g>
<!-- Connector -->
<line x1="100" y1="66" x2="100" y2="86" stroke="#9C9184" stroke-width="1.5"/>
<polygon points="95,82 100,90 105,82" fill="#9C9184"/>
<!-- PROCESS node -->
<g transform="translate(20, 90)">
<rect x="0" y="0" width="160" height="56" rx="8" fill="#C6820E" opacity="0.12" stroke="#C6820E" stroke-width="1.5"/>
<rect x="0" y="0" width="42" height="56" rx="8" fill="#C6820E"/>
<rect x="8" y="0" width="34" height="56" fill="#C6820E"/>
<text x="21" y="34" fill="white" font-size="18" text-anchor="middle">&#9881;</text>
<text x="100" y="25" fill="#1A1612" font-size="12" font-weight="600" text-anchor="middle">LOG</text>
<text x="100" y="42" fill="#5C5347" font-size="11" text-anchor="middle">Processing order</text>
</g>
<!-- Connector -->
<line x1="100" y1="146" x2="100" y2="166" stroke="#9C9184" stroke-width="1.5"/>
<polygon points="95,162 100,170 105,162" fill="#9C9184"/>
<!-- TO node -->
<g transform="translate(20, 170)">
<rect x="0" y="0" width="160" height="56" rx="8" fill="#3D7C47" opacity="0.12" stroke="#3D7C47" stroke-width="1.5"/>
<rect x="0" y="0" width="42" height="56" rx="8" fill="#3D7C47"/>
<rect x="8" y="0" width="34" height="56" fill="#3D7C47"/>
<text x="21" y="34" fill="white" font-size="18" text-anchor="middle">&#9724;</text>
<text x="100" y="25" fill="#1A1612" font-size="12" font-weight="600" text-anchor="middle">TO</text>
<text x="100" y="42" fill="#5C5347" font-size="11" text-anchor="middle">kafka:processed</text>
</g>
<!-- CHOICE compound on the right -->
<g transform="translate(210, 10)">
<rect x="0" y="0" width="180" height="210" rx="10" fill="#7C3AED" opacity="0.06" stroke="#7C3AED" stroke-width="1.5" stroke-dasharray="4 2"/>
<text x="10" y="20" fill="#7C3AED" font-size="11" font-weight="600">CHOICE</text>
<!-- When child -->
<g transform="translate(10, 30)">
<rect x="0" y="0" width="160" height="48" rx="6" fill="#7C3AED" opacity="0.12" stroke="#7C3AED" stroke-width="1"/>
<rect x="0" y="0" width="36" height="48" rx="6" fill="#7C3AED"/>
<rect x="6" y="0" width="30" height="48" fill="#7C3AED"/>
<text x="18" y="30" fill="white" font-size="14" text-anchor="middle">&#9670;</text>
<text x="96" y="20" fill="#1A1612" font-size="11" font-weight="600" text-anchor="middle">WHEN</text>
<text x="96" y="36" fill="#5C5347" font-size="10" text-anchor="middle">header.type == 'A'</text>
</g>
<!-- Otherwise child -->
<g transform="translate(10, 88)">
<rect x="0" y="0" width="160" height="48" rx="6" fill="#7C3AED" opacity="0.12" stroke="#7C3AED" stroke-width="1"/>
<rect x="0" y="0" width="36" height="48" rx="6" fill="#7C3AED"/>
<rect x="6" y="0" width="30" height="48" fill="#7C3AED"/>
<text x="18" y="30" fill="white" font-size="14" text-anchor="middle">&#9670;</text>
<text x="96" y="20" fill="#1A1612" font-size="11" font-weight="600" text-anchor="middle">OTHERWISE</text>
<text x="96" y="36" fill="#5C5347" font-size="10" text-anchor="middle">default branch</text>
</g>
</g>
</svg>
</div>
<div class="card-body">
<h3>A: Icon Sidebar Blocks</h3>
<p>MuleSoft-style: colored icon strip on the left, label + detail on the right. Color encodes node type. Compound nodes (choice, split) use dashed containers.</p>
</div>
</div>
<!-- Option B: Rounded pill with centered icon -->
<div class="card" data-choice="b" onclick="toggleSelect(this)">
<div class="card-image" style="padding: 24px; background: #F5F2ED;">
<svg width="100%" height="220" viewBox="0 0 400 220">
<!-- FROM node -->
<g transform="translate(20, 10)">
<rect x="0" y="0" width="160" height="50" rx="25" fill="#1A7F8E" opacity="0.15" stroke="#1A7F8E" stroke-width="1.5"/>
<circle cx="30" cy="25" r="16" fill="#1A7F8E"/>
<text x="30" y="31" fill="white" font-size="14" text-anchor="middle">&#9654;</text>
<text x="98" y="22" fill="#1A1612" font-size="12" font-weight="600" text-anchor="middle">FROM</text>
<text x="98" y="38" fill="#5C5347" font-size="10" text-anchor="middle">direct:orders</text>
</g>
<!-- Connector -->
<line x1="100" y1="60" x2="100" y2="80" stroke="#9C9184" stroke-width="1.5"/>
<polygon points="95,76 100,84 105,76" fill="#9C9184"/>
<!-- PROCESS node -->
<g transform="translate(20, 84)">
<rect x="0" y="0" width="160" height="50" rx="25" fill="#C6820E" opacity="0.15" stroke="#C6820E" stroke-width="1.5"/>
<circle cx="30" cy="25" r="16" fill="#C6820E"/>
<text x="30" y="31" fill="white" font-size="14" text-anchor="middle">&#9881;</text>
<text x="98" y="22" fill="#1A1612" font-size="12" font-weight="600" text-anchor="middle">LOG</text>
<text x="98" y="38" fill="#5C5347" font-size="10" text-anchor="middle">Processing order</text>
</g>
<!-- Connector -->
<line x1="100" y1="134" x2="100" y2="154" stroke="#9C9184" stroke-width="1.5"/>
<polygon points="95,150 100,158 105,150" fill="#9C9184"/>
<!-- TO node -->
<g transform="translate(20, 158)">
<rect x="0" y="0" width="160" height="50" rx="25" fill="#3D7C47" opacity="0.15" stroke="#3D7C47" stroke-width="1.5"/>
<circle cx="30" cy="25" r="16" fill="#3D7C47"/>
<text x="30" y="31" fill="white" font-size="14" text-anchor="middle">&#9724;</text>
<text x="98" y="22" fill="#1A1612" font-size="12" font-weight="600" text-anchor="middle">TO</text>
<text x="98" y="38" fill="#5C5347" font-size="10" text-anchor="middle">kafka:processed</text>
</g>
<!-- CHOICE compound on the right -->
<g transform="translate(210, 10)">
<rect x="0" y="0" width="180" height="200" rx="12" fill="#7C3AED" opacity="0.06" stroke="#7C3AED" stroke-width="1.5" stroke-dasharray="5 3"/>
<text x="90" y="20" fill="#7C3AED" font-size="11" font-weight="600" text-anchor="middle">CHOICE</text>
<!-- When child -->
<g transform="translate(10, 30)">
<rect x="0" y="0" width="160" height="44" rx="22" fill="#7C3AED" opacity="0.15" stroke="#7C3AED" stroke-width="1"/>
<circle cx="26" cy="22" r="14" fill="#7C3AED"/>
<text x="26" y="28" fill="white" font-size="12" text-anchor="middle">&#9670;</text>
<text x="96" y="18" fill="#1A1612" font-size="11" font-weight="600" text-anchor="middle">WHEN</text>
<text x="96" y="34" fill="#5C5347" font-size="10" text-anchor="middle">type == 'A'</text>
</g>
<!-- Otherwise child -->
<g transform="translate(10, 84)">
<rect x="0" y="0" width="160" height="44" rx="22" fill="#7C3AED" opacity="0.15" stroke="#7C3AED" stroke-width="1"/>
<circle cx="26" cy="22" r="14" fill="#7C3AED"/>
<text x="26" y="28" fill="white" font-size="12" text-anchor="middle">&#9670;</text>
<text x="96" y="18" fill="#1A1612" font-size="11" font-weight="600" text-anchor="middle">OTHERWISE</text>
<text x="96" y="34" fill="#5C5347" font-size="10" text-anchor="middle">default</text>
</g>
</g>
</svg>
</div>
<div class="card-body">
<h3>B: Rounded Pills</h3>
<p>Softer, more modern look with pill-shaped nodes and circular icons. Lighter feel. Compounds still use dashed containers.</p>
</div>
</div>
<!-- Option C: TIBCO BW5 style - rectangular with top color bar -->
<div class="card" data-choice="c" onclick="toggleSelect(this)">
<div class="card-image" style="padding: 24px; background: #F5F2ED;">
<svg width="100%" height="220" viewBox="0 0 400 220">
<!-- FROM node -->
<g transform="translate(20, 10)">
<rect x="0" y="0" width="160" height="56" rx="4" fill="white" stroke="#E4DFD8" stroke-width="1"/>
<rect x="0" y="0" width="160" height="6" rx="4" fill="#1A7F8E"/>
<rect x="4" y="0" width="152" height="6" fill="#1A7F8E"/>
<text x="18" y="32" fill="#1A7F8E" font-size="16">&#9654;</text>
<text x="40" y="30" fill="#1A1612" font-size="12" font-weight="600">FROM</text>
<text x="40" y="46" fill="#5C5347" font-size="10">direct:orders</text>
</g>
<!-- Connector -->
<line x1="100" y1="66" x2="100" y2="86" stroke="#9C9184" stroke-width="1.5"/>
<polygon points="95,82 100,90 105,82" fill="#9C9184"/>
<!-- PROCESS node -->
<g transform="translate(20, 90)">
<rect x="0" y="0" width="160" height="56" rx="4" fill="white" stroke="#E4DFD8" stroke-width="1"/>
<rect x="0" y="0" width="160" height="6" rx="4" fill="#C6820E"/>
<rect x="4" y="0" width="152" height="6" fill="#C6820E"/>
<text x="18" y="32" fill="#C6820E" font-size="16">&#9881;</text>
<text x="40" y="30" fill="#1A1612" font-size="12" font-weight="600">LOG</text>
<text x="40" y="46" fill="#5C5347" font-size="10">Processing order</text>
</g>
<!-- Connector -->
<line x1="100" y1="146" x2="100" y2="166" stroke="#9C9184" stroke-width="1.5"/>
<polygon points="95,162 100,170 105,162" fill="#9C9184"/>
<!-- TO node -->
<g transform="translate(20, 170)">
<rect x="0" y="0" width="160" height="56" rx="4" fill="white" stroke="#E4DFD8" stroke-width="1"/>
<rect x="0" y="0" width="160" height="6" rx="4" fill="#3D7C47"/>
<rect x="4" y="0" width="152" height="6" fill="#3D7C47"/>
<text x="18" y="32" fill="#3D7C47" font-size="16">&#9724;</text>
<text x="40" y="30" fill="#1A1612" font-size="12" font-weight="600">TO</text>
<text x="40" y="46" fill="#5C5347" font-size="10">kafka:processed</text>
</g>
<!-- CHOICE compound on the right -->
<g transform="translate(210, 10)">
<rect x="0" y="0" width="180" height="210" rx="4" fill="white" stroke="#7C3AED" stroke-width="1.5"/>
<rect x="0" y="0" width="180" height="22" rx="4" fill="#7C3AED"/>
<rect x="4" y="4" width="172" height="18" fill="#7C3AED"/>
<text x="90" y="16" fill="white" font-size="11" font-weight="600" text-anchor="middle">CHOICE</text>
<!-- When child -->
<g transform="translate(10, 32)">
<rect x="0" y="0" width="160" height="48" rx="4" fill="white" stroke="#E4DFD8" stroke-width="1"/>
<rect x="0" y="0" width="160" height="5" rx="4" fill="#7C3AED"/>
<rect x="4" y="0" width="152" height="5" fill="#7C3AED"/>
<text x="14" y="28" fill="#7C3AED" font-size="14">&#9670;</text>
<text x="34" y="26" fill="#1A1612" font-size="11" font-weight="600">WHEN</text>
<text x="34" y="40" fill="#5C5347" font-size="10">type == 'A'</text>
</g>
<!-- Otherwise child -->
<g transform="translate(10, 90)">
<rect x="0" y="0" width="160" height="48" rx="4" fill="white" stroke="#E4DFD8" stroke-width="1"/>
<rect x="0" y="0" width="160" height="5" rx="4" fill="#7C3AED"/>
<rect x="4" y="0" width="152" height="5" fill="#7C3AED"/>
<text x="14" y="28" fill="#7C3AED" font-size="14">&#9670;</text>
<text x="34" y="26" fill="#1A1612" font-size="11" font-weight="600">OTHERWISE</text>
<text x="34" y="40" fill="#5C5347" font-size="10">default</text>
</g>
</g>
</svg>
</div>
<div class="card-body">
<h3>C: Top-Bar Cards</h3>
<p>TIBCO BW5-inspired: white cards with colored top accent bar. Clean, professional, card-like. Compound nodes get a full colored header bar with white title text.</p>
</div>
</div>
</div>

View File

@@ -0,0 +1,3 @@
<div style="display:flex;align-items:center;justify-content:center;min-height:60vh">
<p class="subtitle">Continuing in terminal...</p>
</div>

View File

@@ -0,0 +1 @@
{"reason":"idle timeout","timestamp":1774632733532}

View File

@@ -0,0 +1 @@
14618

View File

@@ -0,0 +1,287 @@
<h2>Detail Panel: Tab Designs</h2>
<p class="subtitle">Bottom panel content when a processor node is selected</p>
<div class="mockup">
<div class="mockup-header">Info Tab — processor metadata + attributes</div>
<div class="mockup-body" style="background: #fff; padding: 0;">
<!-- Processor header -->
<div style="display: flex; align-items: center; gap: 10px; padding: 6px 14px; border-bottom: 1px solid #E4DFD8; background: #FAFAF8;">
<span style="font-size: 11px; font-weight: 600; color: #1A1612;">bean:validate</span>
<span style="font-size: 10px; color: #C0392B; background: #FDF2F0; padding: 1px 6px; border-radius: 8px;">FAILED</span>
<span style="font-size: 10px; color: #9C9184;">processor-5</span>
</div>
<!-- Tabs -->
<div style="display: flex; gap: 0; border-bottom: 1px solid #E4DFD8; background: #FAFAF8; padding: 0 14px;">
<div style="font-size: 11px; padding: 6px 12px; color: #C6820E; border-bottom: 2px solid #C6820E; font-weight: 600; cursor: pointer;">Info</div>
<div style="font-size: 11px; padding: 6px 12px; color: #9C9184; cursor: pointer;">Headers</div>
<div style="font-size: 11px; padding: 6px 12px; color: #9C9184; cursor: pointer;">Input</div>
<div style="font-size: 11px; padding: 6px 12px; color: #9C9184; cursor: pointer;">Output</div>
<div style="font-size: 11px; padding: 6px 12px; color: #C0392B; cursor: pointer;">Error</div>
<div style="font-size: 11px; padding: 6px 12px; color: #9C9184; cursor: pointer; opacity: 0.4;">Config</div>
<div style="font-size: 11px; padding: 6px 12px; color: #9C9184; cursor: pointer;">Timeline</div>
</div>
<!-- Info content -->
<div style="padding: 12px 14px; display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 12px 24px; font-size: 12px;">
<div>
<div style="font-size: 10px; color: #9C9184; text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 2px;">Processor ID</div>
<div style="color: #1A1612; font-family: monospace; font-size: 11px;">processor-5</div>
</div>
<div>
<div style="font-size: 10px; color: #9C9184; text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 2px;">Type</div>
<div style="color: #1A1612;">BEAN</div>
</div>
<div>
<div style="font-size: 10px; color: #9C9184; text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 2px;">Status</div>
<div style="color: #C0392B; font-weight: 500;">FAILED</div>
</div>
<div>
<div style="font-size: 10px; color: #9C9184; text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 2px;">Start Time</div>
<div style="color: #1A1612;">14:32:05.123</div>
</div>
<div>
<div style="font-size: 10px; color: #9C9184; text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 2px;">End Time</div>
<div style="color: #1A1612;">14:32:05.243</div>
</div>
<div>
<div style="font-size: 10px; color: #9C9184; text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 2px;">Duration</div>
<div style="color: #1A1612; font-weight: 500;">120ms</div>
</div>
<div>
<div style="font-size: 10px; color: #9C9184; text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 2px;">Endpoint URI</div>
<div style="color: #1A1612; font-family: monospace; font-size: 11px;">bean:orderValidator?method=validate</div>
</div>
<div style="grid-column: span 2;">
<div style="font-size: 10px; color: #9C9184; text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 2px;">Resolved URI</div>
<div style="color: #1A1612; font-family: monospace; font-size: 11px;">bean://com.example.OrderValidator?method=validate</div>
</div>
<!-- Attributes from taps -->
<div style="grid-column: span 3; border-top: 1px solid #E4DFD8; padding-top: 8px; margin-top: 4px;">
<div style="font-size: 10px; color: #9C9184; text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 6px;">Attributes</div>
<div style="display: flex; gap: 6px; flex-wrap: wrap;">
<span style="font-size: 10px; padding: 2px 8px; background: #F5F0EA; border-radius: 10px; color: #5C5347;">orderId: <strong>ORD-1234</strong></span>
<span style="font-size: 10px; padding: 2px 8px; background: #F5F0EA; border-radius: 10px; color: #5C5347;">customer: <strong>Acme Corp</strong></span>
<span style="font-size: 10px; padding: 2px 8px; background: #F5F0EA; border-radius: 10px; color: #5C5347;">priority: <strong>HIGH</strong></span>
</div>
</div>
</div>
</div>
</div>
<div style="margin-top: 20px;"></div>
<div class="mockup">
<div class="mockup-header">Headers Tab — input vs output side by side</div>
<div class="mockup-body" style="background: #fff; padding: 0;">
<div style="display: flex; align-items: center; gap: 10px; padding: 6px 14px; border-bottom: 1px solid #E4DFD8; background: #FAFAF8;">
<span style="font-size: 11px; font-weight: 600; color: #1A1612;">log:incoming</span>
<span style="font-size: 10px; color: #3D7C47; background: #F0F9F1; padding: 1px 6px; border-radius: 8px;">COMPLETED</span>
<span style="font-size: 10px; color: #9C9184;">processor-2</span>
</div>
<div style="display: flex; gap: 0; border-bottom: 1px solid #E4DFD8; background: #FAFAF8; padding: 0 14px;">
<div style="font-size: 11px; padding: 6px 12px; color: #9C9184; cursor: pointer;">Info</div>
<div style="font-size: 11px; padding: 6px 12px; color: #C6820E; border-bottom: 2px solid #C6820E; font-weight: 600; cursor: pointer;">Headers</div>
<div style="font-size: 11px; padding: 6px 12px; color: #9C9184; cursor: pointer;">Input</div>
<div style="font-size: 11px; padding: 6px 12px; color: #9C9184; cursor: pointer;">Output</div>
<div style="font-size: 11px; padding: 6px 12px; color: #9C9184; cursor: pointer; opacity: 0.4;">Error</div>
<div style="font-size: 11px; padding: 6px 12px; color: #9C9184; cursor: pointer; opacity: 0.4;">Config</div>
<div style="font-size: 11px; padding: 6px 12px; color: #9C9184; cursor: pointer;">Timeline</div>
</div>
<!-- Headers side by side -->
<div style="display: flex; gap: 0; padding: 0;">
<!-- Input headers -->
<div style="flex: 1; padding: 10px 14px; border-right: 1px solid #E4DFD8;">
<div style="font-size: 10px; color: #9C9184; text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 8px;">Input Headers</div>
<table style="width: 100%; font-size: 11px; border-collapse: collapse;">
<tr style="border-bottom: 1px solid #F5F0EA;">
<td style="padding: 3px 0; color: #5C5347; font-weight: 500; width: 40%;">Content-Type</td>
<td style="padding: 3px 0; color: #1A1612; font-family: monospace; font-size: 10px;">application/json</td>
</tr>
<tr style="border-bottom: 1px solid #F5F0EA;">
<td style="padding: 3px 0; color: #5C5347; font-weight: 500;">JMSMessageID</td>
<td style="padding: 3px 0; color: #1A1612; font-family: monospace; font-size: 10px;">ID:broker-42</td>
</tr>
<tr style="border-bottom: 1px solid #F5F0EA;">
<td style="padding: 3px 0; color: #5C5347; font-weight: 500;">breadcrumbId</td>
<td style="padding: 3px 0; color: #1A1612; font-family: monospace; font-size: 10px;">abc-123-def</td>
</tr>
<tr>
<td style="padding: 3px 0; color: #5C5347; font-weight: 500;">CamelHttpMethod</td>
<td style="padding: 3px 0; color: #1A1612; font-family: monospace; font-size: 10px;">POST</td>
</tr>
</table>
</div>
<!-- Output headers -->
<div style="flex: 1; padding: 10px 14px;">
<div style="font-size: 10px; color: #9C9184; text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 8px;">Output Headers</div>
<table style="width: 100%; font-size: 11px; border-collapse: collapse;">
<tr style="border-bottom: 1px solid #F5F0EA;">
<td style="padding: 3px 0; color: #5C5347; font-weight: 500; width: 40%;">Content-Type</td>
<td style="padding: 3px 0; color: #1A1612; font-family: monospace; font-size: 10px;">application/json</td>
</tr>
<tr style="border-bottom: 1px solid #F5F0EA;">
<td style="padding: 3px 0; color: #5C5347; font-weight: 500;">JMSMessageID</td>
<td style="padding: 3px 0; color: #1A1612; font-family: monospace; font-size: 10px;">ID:broker-42</td>
</tr>
<tr style="border-bottom: 1px solid #F5F0EA;">
<td style="padding: 3px 0; color: #5C5347; font-weight: 500;">breadcrumbId</td>
<td style="padding: 3px 0; color: #1A1612; font-family: monospace; font-size: 10px;">abc-123-def</td>
</tr>
<tr style="border-bottom: 1px solid #F5F0EA;">
<td style="padding: 3px 0; color: #5C5347; font-weight: 500;">CamelHttpMethod</td>
<td style="padding: 3px 0; color: #1A1612; font-family: monospace; font-size: 10px;">POST</td>
</tr>
<tr>
<td style="padding: 3px 0; color: #5C5347; font-weight: 500; color: #3D7C47;">orderStatus</td>
<td style="padding: 3px 0; color: #3D7C47; font-family: monospace; font-size: 10px;">validated <span style="font-size: 9px; color: #9C9184; font-family: sans-serif;">(new)</span></td>
</tr>
</table>
</div>
</div>
</div>
</div>
<div style="margin-top: 20px;"></div>
<div class="mockup">
<div class="mockup-header">Input Tab — formatted message body</div>
<div class="mockup-body" style="background: #fff; padding: 0;">
<div style="display: flex; align-items: center; gap: 10px; padding: 6px 14px; border-bottom: 1px solid #E4DFD8; background: #FAFAF8;">
<span style="font-size: 11px; font-weight: 600; color: #1A1612;">log:incoming</span>
<span style="font-size: 10px; color: #3D7C47; background: #F0F9F1; padding: 1px 6px; border-radius: 8px;">COMPLETED</span>
<span style="font-size: 10px; color: #9C9184;">processor-2 &middot; 5ms</span>
</div>
<div style="display: flex; gap: 0; border-bottom: 1px solid #E4DFD8; background: #FAFAF8; padding: 0 14px;">
<div style="font-size: 11px; padding: 6px 12px; color: #9C9184; cursor: pointer;">Info</div>
<div style="font-size: 11px; padding: 6px 12px; color: #9C9184; cursor: pointer;">Headers</div>
<div style="font-size: 11px; padding: 6px 12px; color: #C6820E; border-bottom: 2px solid #C6820E; font-weight: 600; cursor: pointer;">Input</div>
<div style="font-size: 11px; padding: 6px 12px; color: #9C9184; cursor: pointer;">Output</div>
<div style="font-size: 11px; padding: 6px 12px; color: #9C9184; cursor: pointer; opacity: 0.4;">Error</div>
<div style="font-size: 11px; padding: 6px 12px; color: #9C9184; cursor: pointer; opacity: 0.4;">Config</div>
<div style="font-size: 11px; padding: 6px 12px; color: #9C9184; cursor: pointer;">Timeline</div>
</div>
<!-- Body content with syntax highlighting -->
<div style="padding: 10px 14px;">
<div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 6px;">
<span style="font-size: 10px; color: #9C9184;">JSON &middot; 234 bytes</span>
<button style="font-size: 9px; padding: 2px 8px; border: 1px solid #E4DFD8; background: #FAFAF8; border-radius: 3px; cursor: pointer; color: #5C5347;">Copy</button>
</div>
<pre style="font-size: 11px; background: #1A1612; color: #E4DFD8; padding: 12px; border-radius: 6px; margin: 0; line-height: 1.6; overflow-x: auto;">{
<span style="color: #1A7F8E;">"orderId"</span>: <span style="color: #C6820E;">"ORD-1234"</span>,
<span style="color: #1A7F8E;">"customer"</span>: {
<span style="color: #1A7F8E;">"name"</span>: <span style="color: #C6820E;">"Acme Corp"</span>,
<span style="color: #1A7F8E;">"id"</span>: <span style="color: #7C3AED;">42</span>
},
<span style="color: #1A7F8E;">"items"</span>: [
{
<span style="color: #1A7F8E;">"product"</span>: <span style="color: #C6820E;">"Widget A"</span>,
<span style="color: #1A7F8E;">"quantity"</span>: <span style="color: #7C3AED;">5</span>,
<span style="color: #1A7F8E;">"price"</span>: <span style="color: #7C3AED;">29.99</span>
}
],
<span style="color: #1A7F8E;">"priority"</span>: <span style="color: #C6820E;">"HIGH"</span>
}</pre>
</div>
</div>
</div>
<div style="margin-top: 20px;"></div>
<div class="mockup">
<div class="mockup-header">Timeline Tab — Gantt-style processor durations</div>
<div class="mockup-body" style="background: #fff; padding: 0;">
<div style="display: flex; align-items: center; gap: 10px; padding: 6px 14px; border-bottom: 1px solid #E4DFD8; background: #FAFAF8;">
<span style="font-size: 11px; font-weight: 600; color: #1A1612;">content-based-routing</span>
<span style="font-size: 10px; color: #C0392B; background: #FDF2F0; padding: 1px 6px; border-radius: 8px;">FAILED</span>
<span style="font-size: 10px; color: #9C9184;">247ms total</span>
</div>
<div style="display: flex; gap: 0; border-bottom: 1px solid #E4DFD8; background: #FAFAF8; padding: 0 14px;">
<div style="font-size: 11px; padding: 6px 12px; color: #9C9184; cursor: pointer;">Info</div>
<div style="font-size: 11px; padding: 6px 12px; color: #9C9184; cursor: pointer;">Headers</div>
<div style="font-size: 11px; padding: 6px 12px; color: #9C9184; cursor: pointer;">Input</div>
<div style="font-size: 11px; padding: 6px 12px; color: #9C9184; cursor: pointer;">Output</div>
<div style="font-size: 11px; padding: 6px 12px; color: #C0392B; cursor: pointer;">Error</div>
<div style="font-size: 11px; padding: 6px 12px; color: #9C9184; cursor: pointer; opacity: 0.4;">Config</div>
<div style="font-size: 11px; padding: 6px 12px; color: #C6820E; border-bottom: 2px solid #C6820E; font-weight: 600; cursor: pointer;">Timeline</div>
</div>
<!-- Gantt chart -->
<div style="padding: 10px 14px;">
<!-- Time axis -->
<div style="display: flex; justify-content: space-between; font-size: 9px; color: #9C9184; margin-bottom: 4px; padding-left: 110px;">
<span>0ms</span><span>50ms</span><span>100ms</span><span>150ms</span><span>200ms</span><span>247ms</span>
</div>
<!-- Processor rows -->
<div style="display: flex; flex-direction: column; gap: 3px;">
<!-- from:jms -->
<div style="display: flex; align-items: center; gap: 8px;">
<span style="font-size: 10px; color: #5C5347; width: 100px; text-align: right; flex-shrink: 0;">from:jms</span>
<div style="flex: 1; height: 16px; background: #F5F0EA; border-radius: 2px; position: relative;">
<div style="position: absolute; left: 0%; width: 0.8%; height: 100%; background: #3D7C47; border-radius: 2px; min-width: 3px;"></div>
</div>
<span style="font-size: 9px; color: #9C9184; width: 36px; flex-shrink: 0;">2ms</span>
</div>
<!-- log -->
<div style="display: flex; align-items: center; gap: 8px;">
<span style="font-size: 10px; color: #5C5347; width: 100px; text-align: right; flex-shrink: 0;">log</span>
<div style="flex: 1; height: 16px; background: #F5F0EA; border-radius: 2px; position: relative;">
<div style="position: absolute; left: 0.8%; width: 2%; height: 100%; background: #3D7C47; border-radius: 2px; min-width: 3px;"></div>
</div>
<span style="font-size: 9px; color: #9C9184; width: 36px; flex-shrink: 0;">5ms</span>
</div>
<!-- setHeader -->
<div style="display: flex; align-items: center; gap: 8px;">
<span style="font-size: 10px; color: #5C5347; width: 100px; text-align: right; flex-shrink: 0;">setHeader</span>
<div style="flex: 1; height: 16px; background: #F5F0EA; border-radius: 2px; position: relative;">
<div style="position: absolute; left: 2.8%; width: 0.4%; height: 100%; background: #3D7C47; border-radius: 2px; min-width: 3px;"></div>
</div>
<span style="font-size: 9px; color: #9C9184; width: 36px; flex-shrink: 0;">1ms</span>
</div>
<!-- bean:validate (FAILED - long) -->
<div style="display: flex; align-items: center; gap: 8px;">
<span style="font-size: 10px; color: #C0392B; font-weight: 600; width: 100px; text-align: right; flex-shrink: 0;">bean:validate</span>
<div style="flex: 1; height: 16px; background: #F5F0EA; border-radius: 2px; position: relative;">
<div style="position: absolute; left: 3.2%; width: 48.6%; height: 100%; background: #C0392B; border-radius: 2px; opacity: 0.8;"></div>
</div>
<span style="font-size: 9px; color: #C0392B; font-weight: 500; width: 36px; flex-shrink: 0;">120ms</span>
</div>
<!-- to:http (skipped) -->
<div style="display: flex; align-items: center; gap: 8px; opacity: 0.35;">
<span style="font-size: 10px; color: #5C5347; width: 100px; text-align: right; flex-shrink: 0;">to:http</span>
<div style="flex: 1; height: 16px; background: #F5F0EA; border-radius: 2px;"></div>
<span style="font-size: 9px; color: #9C9184; width: 36px; flex-shrink: 0;"></span>
</div>
<!-- to:jms (skipped) -->
<div style="display: flex; align-items: center; gap: 8px; opacity: 0.35;">
<span style="font-size: 10px; color: #5C5347; width: 100px; text-align: right; flex-shrink: 0;">to:jms</span>
<div style="flex: 1; height: 16px; background: #F5F0EA; border-radius: 2px;"></div>
<span style="font-size: 9px; color: #9C9184; width: 36px; flex-shrink: 0;"></span>
</div>
</div>
<div style="margin-top: 8px; font-size: 10px; color: #9C9184;">Click a bar to select that processor in the diagram</div>
</div>
</div>
</div>
<div style="margin-top: 20px;"></div>
<div class="mockup">
<div class="mockup-header">Error Tab — grayed out when no error on selected processor</div>
<div class="mockup-body" style="background: #fff; padding: 0;">
<div style="display: flex; align-items: center; gap: 10px; padding: 6px 14px; border-bottom: 1px solid #E4DFD8; background: #FAFAF8;">
<span style="font-size: 11px; font-weight: 600; color: #1A1612;">log:incoming</span>
<span style="font-size: 10px; color: #3D7C47; background: #F0F9F1; padding: 1px 6px; border-radius: 8px;">COMPLETED</span>
<span style="font-size: 10px; color: #9C9184;">processor-2 &middot; 5ms</span>
</div>
<div style="display: flex; gap: 0; border-bottom: 1px solid #E4DFD8; background: #FAFAF8; padding: 0 14px;">
<div style="font-size: 11px; padding: 6px 12px; color: #9C9184; cursor: pointer;">Info</div>
<div style="font-size: 11px; padding: 6px 12px; color: #9C9184; cursor: pointer;">Headers</div>
<div style="font-size: 11px; padding: 6px 12px; color: #9C9184; cursor: pointer;">Input</div>
<div style="font-size: 11px; padding: 6px 12px; color: #9C9184; cursor: pointer;">Output</div>
<div style="font-size: 11px; padding: 6px 12px; color: #9C9184; cursor: pointer; opacity: 0.4; cursor: not-allowed;">Error</div>
<div style="font-size: 11px; padding: 6px 12px; color: #9C9184; cursor: pointer; opacity: 0.4;">Config</div>
<div style="font-size: 11px; padding: 6px 12px; color: #9C9184; cursor: pointer;">Timeline</div>
</div>
<div style="padding: 20px 14px; text-align: center; color: #9C9184; font-size: 12px;">
No error on this processor
</div>
</div>
</div>

View File

@@ -0,0 +1,207 @@
<h2>Execution Overlay: Full Design Mockup</h2>
<p class="subtitle">ExecutionDiagram component — diagram with execution overlay + detail panel</p>
<div class="mockup">
<div class="mockup-header">ExecutionDiagram — Failed Exchange View</div>
<div class="mockup-body" style="background: #FAFAF8; padding: 0;">
<!-- Top bar: Exchange summary -->
<div style="display: flex; align-items: center; gap: 12px; padding: 8px 14px; background: #fff; border-bottom: 1px solid #E4DFD8; font-size: 12px; color: #5C5347;">
<span style="font-weight: 600; color: #1A1612;">Exchange</span>
<code style="font-size: 11px; background: #F5F0EA; padding: 2px 6px; border-radius: 3px; color: #1A1612;">abc-123-def-456</code>
<span style="background: #C0392B; color: white; font-size: 10px; padding: 1px 8px; border-radius: 10px; font-weight: 600;">FAILED</span>
<span style="color: #9C9184;">sample-app / content-based-routing</span>
<span style="color: #9C9184;">247ms</span>
<div style="margin-left: auto; display: flex; gap: 6px;">
<button style="font-size: 10px; padding: 3px 10px; border: 1px solid #C0392B; background: #FDF2F0; color: #C0392B; border-radius: 4px; cursor: pointer; font-weight: 500;">Jump to Error</button>
</div>
</div>
<!-- Main content: Diagram top, Detail bottom -->
<div style="display: flex; flex-direction: column; height: 480px;">
<!-- TOP: Process Diagram with Overlay -->
<div style="flex: 1; position: relative; overflow: hidden; background: #fff; border-bottom: 2px solid #E4DFD8;">
<!-- Breadcrumbs (if drilled down) -->
<!-- Diagram content -->
<div style="padding: 24px 30px;">
<!-- Main flow -->
<div style="display: flex; align-items: center; gap: 0;">
<!-- Node: from:jms (COMPLETED) -->
<div style="position: relative;">
<div style="width: 140px; height: 52px; background: #fff; border: 1px solid #E4DFD8; border-radius: 6px; border-left: 4px solid #3D7C47; overflow: hidden;">
<div style="height: 5px; background: #1A7F8E;"></div>
<div style="padding: 4px 8px;">
<div style="font-size: 10px; font-weight: 600; color: #1A1612;">from:jms:orders</div>
<div style="font-size: 9px; color: #9C9184;">ENDPOINT</div>
</div>
</div>
<div style="position: absolute; bottom: 2px; right: 6px; font-size: 8px; color: #3D7C47; font-weight: 500;">2ms</div>
</div>
<!-- Edge -->
<svg width="30" height="10" style="flex-shrink:0;"><line x1="0" y1="5" x2="30" y2="5" stroke="#9CA3AF" stroke-width="1.5"/><polygon points="25,2 30,5 25,8" fill="#9CA3AF"/></svg>
<!-- Node: log (COMPLETED) -->
<div style="position: relative;">
<div style="width: 140px; height: 52px; background: #fff; border: 1px solid #E4DFD8; border-radius: 6px; border-left: 4px solid #3D7C47; overflow: hidden;">
<div style="height: 5px; background: #C6820E;"></div>
<div style="padding: 4px 8px;">
<div style="font-size: 10px; font-weight: 600; color: #1A1612;">log:incoming</div>
<div style="font-size: 9px; color: #9C9184;">LOG</div>
</div>
</div>
<div style="position: absolute; bottom: 2px; right: 6px; font-size: 8px; color: #3D7C47; font-weight: 500;">5ms</div>
</div>
<!-- Edge -->
<svg width="30" height="10" style="flex-shrink:0;"><line x1="0" y1="5" x2="30" y2="5" stroke="#9CA3AF" stroke-width="1.5"/><polygon points="25,2 30,5 25,8" fill="#9CA3AF"/></svg>
<!-- Node: CHOICE compound -->
<div style="position: relative; border: 2px dashed #7C3AED; border-radius: 8px; padding: 0; background: #FAFAFF;">
<!-- Compound header -->
<div style="background: #7C3AED; color: white; font-size: 10px; font-weight: 600; padding: 3px 10px; border-radius: 5px 5px 0 0;">CHOICE</div>
<div style="padding: 10px; display: flex; gap: 16px;">
<!-- WHEN branch (taken, failed) -->
<div style="border: 1px solid #E4DFD8; border-radius: 5px; padding: 6px; background: #fff;">
<div style="font-size: 8px; color: #7C3AED; font-weight: 600; margin-bottom: 4px;">WHEN: header.type == 'A'</div>
<div style="display: flex; align-items: center; gap: 0;">
<!-- Node: bean (FAILED) -->
<div style="position: relative;">
<div style="width: 120px; height: 48px; background: #FDF2F0; border: 2px solid #C0392B; border-radius: 6px; overflow: hidden;">
<div style="height: 5px; background: #C6820E;"></div>
<div style="padding: 3px 6px;">
<div style="font-size: 9px; font-weight: 600; color: #C0392B;">bean:validate</div>
<div style="font-size: 8px; color: #C0392B;">FAILED</div>
</div>
</div>
<div style="position: absolute; bottom: 1px; right: 4px; font-size: 7px; color: #C0392B; font-weight: 500;">120ms</div>
<!-- Error icon -->
<div style="position: absolute; top: -6px; right: -6px; width: 16px; height: 16px; background: #C0392B; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 9px; color: white; font-weight: bold;">!</div>
</div>
<svg width="20" height="10" style="flex-shrink:0;"><line x1="0" y1="5" x2="20" y2="5" stroke="#9CA3AF" stroke-width="1"/></svg>
<!-- Node: to:http (NOT EXECUTED - dimmed) -->
<div style="position: relative; opacity: 0.35;">
<div style="width: 120px; height: 48px; background: #fff; border: 1px solid #E4DFD8; border-radius: 6px; overflow: hidden;">
<div style="height: 5px; background: #3D7C47;"></div>
<div style="padding: 3px 6px;">
<div style="font-size: 9px; font-weight: 600; color: #1A1612;">to:http:api</div>
<div style="font-size: 8px; color: #9C9184;">TO</div>
</div>
</div>
</div>
</div>
</div>
<!-- OTHERWISE branch (not taken - dimmed) -->
<div style="border: 1px solid #E4DFD8; border-radius: 5px; padding: 6px; background: #fff; opacity: 0.35;">
<div style="font-size: 8px; color: #7C3AED; font-weight: 600; margin-bottom: 4px;">OTHERWISE</div>
<div style="width: 120px; height: 48px; background: #fff; border: 1px solid #E4DFD8; border-radius: 6px; overflow: hidden;">
<div style="height: 5px; background: #3D7C47;"></div>
<div style="padding: 3px 6px;">
<div style="font-size: 9px; font-weight: 600; color: #1A1612;">to:direct:alt</div>
<div style="font-size: 8px; color: #9C9184;">DIRECT</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Zoom controls (bottom-right) -->
<div style="position: absolute; bottom: 8px; right: 8px; display: flex; align-items: center; gap: 3px; background: #fff; border: 1px solid #E4DFD8; border-radius: 4px; padding: 3px; box-shadow: 0 1px 4px rgba(0,0,0,0.06);">
<button style="width: 22px; height: 22px; border: none; background: transparent; font-size: 12px; cursor: pointer; color: #1A1612;">+</button>
<span style="font-size: 9px; color: #9C9184; min-width: 30px; text-align: center;">100%</span>
<button style="width: 22px; height: 22px; border: none; background: transparent; font-size: 12px; cursor: pointer; color: #1A1612;">-</button>
<button style="width: 22px; height: 22px; border: none; background: transparent; font-size: 10px; cursor: pointer; color: #1A1612;">Fit</button>
</div>
<!-- Minimap (bottom-left) -->
<div style="position: absolute; bottom: 8px; left: 8px; width: 100px; height: 60px; background: #fff; border: 1px solid #E4DFD8; border-radius: 4px; box-shadow: 0 1px 4px rgba(0,0,0,0.06); overflow: hidden;">
<div style="padding: 4px;">
<div style="display: flex; gap: 2px; align-items: center; transform: scale(0.3); transform-origin: top left;">
<div style="width: 60px; height: 20px; background: #1A7F8E; border-radius: 2px;"></div>
<div style="width: 60px; height: 20px; background: #C6820E; border-radius: 2px;"></div>
<div style="width: 100px; height: 40px; border: 1px solid #7C3AED; border-radius: 2px;"></div>
</div>
</div>
</div>
</div>
<!-- SPLITTER -->
<div style="height: 4px; background: #E4DFD8; cursor: row-resize; flex-shrink: 0;"></div>
<!-- BOTTOM: Detail Panel -->
<div style="flex: 0 0 180px; background: #fff; display: flex; flex-direction: column;">
<!-- Selected processor header -->
<div style="display: flex; align-items: center; gap: 10px; padding: 6px 14px; border-bottom: 1px solid #E4DFD8; background: #FAFAF8;">
<span style="font-size: 11px; font-weight: 600; color: #C0392B;">bean:validate</span>
<span style="font-size: 10px; color: #C0392B; background: #FDF2F0; padding: 1px 6px; border-radius: 8px;">FAILED</span>
<span style="font-size: 10px; color: #9C9184;">processor-5 &middot; 120ms</span>
</div>
<!-- Tabs -->
<div style="display: flex; gap: 0; border-bottom: 1px solid #E4DFD8; background: #FAFAF8; padding: 0 14px;">
<div style="font-size: 11px; padding: 6px 12px; color: #9C9184; cursor: pointer;">Info</div>
<div style="font-size: 11px; padding: 6px 12px; color: #9C9184; cursor: pointer;">Headers</div>
<div style="font-size: 11px; padding: 6px 12px; color: #9C9184; cursor: pointer;">Input</div>
<div style="font-size: 11px; padding: 6px 12px; color: #9C9184; cursor: pointer;">Output</div>
<div style="font-size: 11px; padding: 6px 12px; color: #C0392B; border-bottom: 2px solid #C0392B; font-weight: 600; cursor: pointer;">Error</div>
<div style="font-size: 11px; padding: 6px 12px; color: #9C9184; cursor: pointer; opacity: 0.5;">Config</div>
<div style="font-size: 11px; padding: 6px 12px; color: #9C9184; cursor: pointer;">Timeline</div>
</div>
<!-- Tab content: Error -->
<div style="flex: 1; padding: 10px 14px; overflow-y: auto;">
<div style="margin-bottom: 8px;">
<div style="font-size: 10px; color: #9C9184; text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 2px;">Exception</div>
<div style="font-size: 12px; color: #C0392B; font-weight: 500;">javax.validation.ValidationException</div>
</div>
<div style="margin-bottom: 8px;">
<div style="font-size: 10px; color: #9C9184; text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 2px;">Message</div>
<div style="font-size: 12px; color: #1A1612;">Order quantity must be positive: received -3</div>
</div>
<div style="margin-bottom: 8px;">
<div style="font-size: 10px; color: #9C9184; text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 2px;">Root Cause</div>
<div style="font-size: 12px; color: #C0392B;">java.lang.IllegalArgumentException: quantity must be > 0</div>
</div>
<div>
<div style="font-size: 10px; color: #9C9184; text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 2px;">Stack Trace</div>
<pre style="font-size: 10px; color: #5C5347; background: #F5F0EA; padding: 8px; border-radius: 4px; overflow-x: auto; margin: 0; line-height: 1.6;">at com.example.OrderValidator.validate(OrderValidator.java:42)
at com.example.OrderRoute.process(OrderRoute.java:18)
at org.apache.camel.processor.DelegateSyncProcessor.process(...)
at org.apache.camel.processor.Pipeline.process(Pipeline.java:184)
... 8 more</pre>
</div>
</div>
</div>
</div>
</div>
</div>
<div style="margin-top: 24px;">
<h3>Design Decisions Shown</h3>
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 12px; font-size: 13px; color: #5C5347;">
<div style="background: #f8f8f6; padding: 10px; border-radius: 6px; border-left: 3px solid #3D7C47;">
<strong style="color: #1A1612;">Executed (OK)</strong><br/>
Green left border, duration badge bottom-right
</div>
<div style="background: #f8f8f6; padding: 10px; border-radius: 6px; border-left: 3px solid #C0392B;">
<strong style="color: #1A1612;">Failed</strong><br/>
Red border, red tint background, red ! badge top-right
</div>
<div style="background: #f8f8f6; padding: 10px; border-radius: 6px; border-left: 3px solid #9C9184;">
<strong style="color: #1A1612;">Not Executed</strong><br/>
Dimmed to 35% opacity — full topology visible
</div>
<div style="background: #f8f8f6; padding: 10px; border-radius: 6px; border-left: 3px solid #C6820E;">
<strong style="color: #1A1612;">Selected</strong><br/>
Amber ring (existing), detail panel updates below
</div>
</div>
</div>

View File

@@ -0,0 +1,143 @@
<h2>Per-Compound Iteration Stepper</h2>
<p class="subtitle">Each loop/split compound gets its own stepper in the header bar</p>
<div class="mockup">
<div class="mockup-header">Loop with iteration stepper — iteration 3 of 5</div>
<div class="mockup-body" style="background: #FAFAF8; padding: 20px;">
<!-- LOOP compound -->
<div style="position: relative; border: 2px dashed #7C3AED; border-radius: 8px; background: #FAFAFF; max-width: 600px;">
<!-- Compound header with stepper -->
<div style="background: #7C3AED; color: white; font-size: 11px; font-weight: 600; padding: 4px 10px; border-radius: 5px 5px 0 0; display: flex; align-items: center; justify-content: space-between;">
<span>LOOP</span>
<!-- Iteration stepper -->
<div style="display: flex; align-items: center; gap: 4px; background: rgba(255,255,255,0.15); border-radius: 3px; padding: 1px 4px;">
<button style="width: 18px; height: 18px; border: none; background: rgba(255,255,255,0.2); color: white; border-radius: 2px; cursor: pointer; font-size: 10px; display: flex; align-items: center; justify-content: center;">&lsaquo;</button>
<span style="font-size: 10px; min-width: 30px; text-align: center; font-variant-numeric: tabular-nums;">3 / 5</span>
<button style="width: 18px; height: 18px; border: none; background: rgba(255,255,255,0.2); color: white; border-radius: 2px; cursor: pointer; font-size: 10px; display: flex; align-items: center; justify-content: center;">&rsaquo;</button>
</div>
</div>
<!-- Loop body: showing iteration 3 data -->
<div style="padding: 12px; display: flex; align-items: center; gap: 0;">
<!-- transform (OK in iteration 3) -->
<div style="position: relative;">
<div style="width: 130px; height: 48px; background: #F0F9F1; border: 1.5px solid #3D7C47; border-radius: 5px; border-left: 3px solid #3D7C47; overflow: hidden;">
<div style="height: 4px; background: #C6820E;"></div>
<div style="padding: 3px 6px;">
<div style="font-size: 9px; font-weight: 600; color: #1A1612;">transform</div>
<div style="font-size: 8px; color: #9C9184;">TRANSFORM</div>
</div>
</div>
<div style="position: absolute; bottom: 1px; right: 4px; font-size: 7px; color: #3D7C47;">3ms</div>
<div style="position: absolute; top: -5px; right: -5px; width: 13px; height: 13px; background: #3D7C47; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 8px; color: white;">&#10003;</div>
</div>
<svg width="24" height="10" style="flex-shrink:0;"><line x1="0" y1="5" x2="20" y2="5" stroke="#3D7C47" stroke-width="1.5"/><polygon points="17,2 22,5 17,8" fill="#3D7C47"/></svg>
<!-- to:http (OK in iteration 3) -->
<div style="position: relative;">
<div style="width: 130px; height: 48px; background: #F0F9F1; border: 1.5px solid #3D7C47; border-radius: 5px; border-left: 3px solid #3D7C47; overflow: hidden;">
<div style="height: 4px; background: #3D7C47;"></div>
<div style="padding: 3px 6px;">
<div style="font-size: 9px; font-weight: 600; color: #1A1612;">to:http:api</div>
<div style="font-size: 8px; color: #9C9184;">TO</div>
</div>
</div>
<div style="position: absolute; bottom: 1px; right: 4px; font-size: 7px; color: #3D7C47;">45ms</div>
<div style="position: absolute; top: -5px; right: -5px; width: 13px; height: 13px; background: #3D7C47; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 8px; color: white;">&#10003;</div>
</div>
<svg width="24" height="10" style="flex-shrink:0;"><line x1="0" y1="5" x2="20" y2="5" stroke="#3D7C47" stroke-width="1.5"/><polygon points="17,2 22,5 17,8" fill="#3D7C47"/></svg>
<!-- log (OK in iteration 3) -->
<div style="position: relative;">
<div style="width: 130px; height: 48px; background: #F0F9F1; border: 1.5px solid #3D7C47; border-radius: 5px; border-left: 3px solid #3D7C47; overflow: hidden;">
<div style="height: 4px; background: #C6820E;"></div>
<div style="padding: 3px 6px;">
<div style="font-size: 9px; font-weight: 600; color: #1A1612;">log:result</div>
<div style="font-size: 8px; color: #9C9184;">LOG</div>
</div>
</div>
<div style="position: absolute; bottom: 1px; right: 4px; font-size: 7px; color: #3D7C47;">1ms</div>
<div style="position: absolute; top: -5px; right: -5px; width: 13px; height: 13px; background: #3D7C47; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 8px; color: white;">&#10003;</div>
</div>
</div>
</div>
</div>
</div>
<h3 style="margin-top: 24px;">Nested Loops</h3>
<p class="subtitle">Each compound level has its own independent stepper</p>
<div class="mockup">
<div class="mockup-header">Outer loop (iteration 2/3) containing inner split (branch 1/4)</div>
<div class="mockup-body" style="background: #FAFAF8; padding: 20px;">
<!-- Outer LOOP -->
<div style="border: 2px dashed #7C3AED; border-radius: 8px; background: #FAFAFF; max-width: 550px;">
<div style="background: #7C3AED; color: white; font-size: 11px; font-weight: 600; padding: 4px 10px; border-radius: 5px 5px 0 0; display: flex; align-items: center; justify-content: space-between;">
<span>LOOP</span>
<div style="display: flex; align-items: center; gap: 4px; background: rgba(255,255,255,0.15); border-radius: 3px; padding: 1px 4px;">
<button style="width: 18px; height: 18px; border: none; background: rgba(255,255,255,0.2); color: white; border-radius: 2px; cursor: pointer; font-size: 10px; display: flex; align-items: center; justify-content: center;">&lsaquo;</button>
<span style="font-size: 10px; min-width: 30px; text-align: center;">2 / 3</span>
<button style="width: 18px; height: 18px; border: none; background: rgba(255,255,255,0.2); color: white; border-radius: 2px; cursor: pointer; font-size: 10px; display: flex; align-items: center; justify-content: center;">&rsaquo;</button>
</div>
</div>
<div style="padding: 12px;">
<div style="display: flex; align-items: center; gap: 0;">
<!-- Processor before split -->
<div style="position: relative;">
<div style="width: 110px; height: 44px; background: #F0F9F1; border: 1.5px solid #3D7C47; border-radius: 5px; border-left: 3px solid #3D7C47; overflow: hidden;">
<div style="height: 4px; background: #C6820E;"></div>
<div style="padding: 3px 6px;">
<div style="font-size: 9px; font-weight: 600; color: #1A1612;">setBody</div>
<div style="font-size: 8px; color: #9C9184;">SET_BODY</div>
</div>
</div>
<div style="position: absolute; top: -5px; right: -5px; width: 13px; height: 13px; background: #3D7C47; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 8px; color: white;">&#10003;</div>
</div>
<svg width="20" height="10" style="flex-shrink:0;"><line x1="0" y1="5" x2="16" y2="5" stroke="#3D7C47" stroke-width="1.5"/></svg>
<!-- Inner SPLIT -->
<div style="border: 2px dashed #7C3AED; border-radius: 6px; background: #F8F7FF;">
<div style="background: #9B6AED; color: white; font-size: 10px; font-weight: 600; padding: 3px 8px; border-radius: 3px 3px 0 0; display: flex; align-items: center; justify-content: space-between;">
<span>SPLIT</span>
<div style="display: flex; align-items: center; gap: 3px; background: rgba(255,255,255,0.15); border-radius: 3px; padding: 1px 3px;">
<button style="width: 16px; height: 16px; border: none; background: rgba(255,255,255,0.2); color: white; border-radius: 2px; cursor: pointer; font-size: 9px; display: flex; align-items: center; justify-content: center;">&lsaquo;</button>
<span style="font-size: 9px; min-width: 26px; text-align: center;">1 / 4</span>
<button style="width: 16px; height: 16px; border: none; background: rgba(255,255,255,0.2); color: white; border-radius: 2px; cursor: pointer; font-size: 9px; display: flex; align-items: center; justify-content: center;">&rsaquo;</button>
</div>
</div>
<div style="padding: 8px; display: flex; align-items: center; gap: 0;">
<div style="position: relative;">
<div style="width: 100px; height: 40px; background: #F0F9F1; border: 1.5px solid #3D7C47; border-radius: 4px; border-left: 3px solid #3D7C47; overflow: hidden;">
<div style="height: 3px; background: #3D7C47;"></div>
<div style="padding: 2px 5px;">
<div style="font-size: 8px; font-weight: 600; color: #1A1612;">to:kafka</div>
<div style="font-size: 7px; color: #9C9184;">TO</div>
</div>
</div>
<div style="position: absolute; top: -4px; right: -4px; width: 12px; height: 12px; background: #3D7C47; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 7px; color: white;">&#10003;</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<h3 style="margin-top: 24px;">Stepper Behavior</h3>
<div style="font-size: 13px; color: #5C5347; line-height: 1.8;">
<ul style="margin: 0; padding-left: 20px;">
<li><strong>Independent per compound</strong> — outer loop at iteration 2, inner split at branch 1</li>
<li><strong>Overlay updates per-compound</strong> — stepping the loop re-renders its children's execution data for that iteration</li>
<li><strong>CHOICE shows which branch was taken</strong> — no stepper, just highlights the taken branch</li>
<li><strong>Keyboard</strong> — when a compound is focused/hovered, left/right arrow keys step through iterations</li>
<li><strong>Detail panel syncs</strong> — selecting a processor inside a loop shows that iteration's data</li>
</ul>
</div>

View File

@@ -0,0 +1,166 @@
<h2>Execution Overlay: Page Layout</h2>
<p class="subtitle">How should the diagram + execution details be arranged?</p>
<div class="cards">
<!-- Option A: Horizontal Split -->
<div class="card" data-choice="a" onclick="toggleSelect(this)">
<div class="card-image">
<div style="padding: 16px; background: #1a1612; border-radius: 6px;">
<!-- IDE-style: diagram top, detail bottom -->
<div style="display: flex; flex-direction: column; gap: 8px; height: 280px;">
<!-- Top: Diagram -->
<div style="flex: 1; background: #2a2520; border-radius: 4px; padding: 12px; position: relative; overflow: hidden;">
<div style="font-size: 10px; color: #9C9184; margin-bottom: 8px;">DIAGRAM</div>
<!-- Mini route flow mockup -->
<div style="display: flex; align-items: center; gap: 6px; margin-left: 20px;">
<div style="width: 60px; height: 28px; background: #1A7F8E; border-radius: 3px; display: flex; align-items: center; justify-content: center; font-size: 8px; color: white;">from:jms</div>
<div style="width: 20px; height: 1px; background: #5C5347;"></div>
<div style="width: 60px; height: 28px; background: #C6820E; border-radius: 3px; display: flex; align-items: center; justify-content: center; font-size: 8px; color: white; border: 2px solid #3D7C47;">log</div>
<div style="width: 20px; height: 1px; background: #5C5347;"></div>
<div style="width: 60px; height: 28px; background: #C0392B; border-radius: 3px; display: flex; align-items: center; justify-content: center; font-size: 8px; color: white; border: 2px solid #C0392B; opacity: 0.9;">bean</div>
<div style="width: 20px; height: 1px; background: #5C5347;"></div>
<div style="width: 60px; height: 28px; background: #3D7C47; border-radius: 3px; display: flex; align-items: center; justify-content: center; font-size: 8px; color: white; opacity: 0.4;">to:http</div>
</div>
<!-- Zoom controls hint -->
<div style="position: absolute; bottom: 6px; right: 6px; font-size: 8px; color: #5C5347; background: #2a2520; padding: 2px 6px; border: 1px solid #3a3530; border-radius: 3px;">100%</div>
<!-- Iteration stepper -->
<div style="position: absolute; top: 6px; right: 6px; font-size: 8px; color: #C6820E; background: #2a2520; padding: 2px 8px; border: 1px solid #3a3530; border-radius: 3px;">Loop 2/5</div>
</div>
<!-- Resizable splitter -->
<div style="height: 3px; background: #3a3530; border-radius: 2px; cursor: row-resize;"></div>
<!-- Bottom: Details -->
<div style="flex: 0 0 100px; background: #2a2520; border-radius: 4px; padding: 8px; overflow: hidden;">
<div style="display: flex; gap: 12px; font-size: 9px; color: #9C9184; border-bottom: 1px solid #3a3530; padding-bottom: 4px; margin-bottom: 6px;">
<span style="color: #C6820E; border-bottom: 2px solid #C6820E; padding-bottom: 3px;">Input</span>
<span>Output</span>
<span>Headers</span>
<span>Error</span>
<span>Timeline</span>
</div>
<div style="font-family: monospace; font-size: 8px; color: #9C9184; line-height: 1.5;">
<div>{"orderId": "ORD-1234",</div>
<div>&nbsp;"product": "Widget A",</div>
<div>&nbsp;"quantity": 5}</div>
</div>
</div>
</div>
</div>
</div>
<div class="card-body">
<h3>A: Top/Bottom Split (IDE Style)</h3>
<p>Diagram on top, tabbed detail panel below. Resizable splitter between them. Maximizes diagram width. Tabs: Input, Output, Headers, Error, Timeline.</p>
<div class="pros-cons">
<div class="pros"><h4>Pros</h4><ul><li>Full diagram width</li><li>Familiar IDE pattern</li><li>Detail panel always visible</li></ul></div>
<div class="cons"><h4>Cons</h4><ul><li>Vertical space shared</li><li>Diagram shrinks on small screens</li></ul></div>
</div>
</div>
</div>
<!-- Option B: Right Panel -->
<div class="card" data-choice="b" onclick="toggleSelect(this)">
<div class="card-image">
<div style="padding: 16px; background: #1a1612; border-radius: 6px;">
<div style="display: flex; gap: 8px; height: 280px;">
<!-- Left: Diagram -->
<div style="flex: 1; background: #2a2520; border-radius: 4px; padding: 12px; position: relative; overflow: hidden;">
<div style="font-size: 10px; color: #9C9184; margin-bottom: 8px;">DIAGRAM</div>
<div style="display: flex; align-items: center; gap: 6px; margin-left: 10px;">
<div style="width: 55px; height: 26px; background: #1A7F8E; border-radius: 3px; display: flex; align-items: center; justify-content: center; font-size: 7px; color: white;">from:jms</div>
<div style="width: 14px; height: 1px; background: #5C5347;"></div>
<div style="width: 55px; height: 26px; background: #C6820E; border-radius: 3px; display: flex; align-items: center; justify-content: center; font-size: 7px; color: white; border: 2px solid #3D7C47;">log</div>
<div style="width: 14px; height: 1px; background: #5C5347;"></div>
<div style="width: 55px; height: 26px; background: #C0392B; border-radius: 3px; display: flex; align-items: center; justify-content: center; font-size: 7px; color: white; border: 2px solid #C0392B;">bean</div>
</div>
<div style="position: absolute; bottom: 6px; right: 6px; font-size: 8px; color: #5C5347; background: #2a2520; padding: 2px 6px; border: 1px solid #3a3530; border-radius: 3px;">100%</div>
</div>
<!-- Resizable splitter -->
<div style="width: 3px; background: #3a3530; border-radius: 2px; cursor: col-resize;"></div>
<!-- Right: Detail Panel -->
<div style="flex: 0 0 200px; background: #2a2520; border-radius: 4px; padding: 8px; overflow: hidden;">
<div style="font-size: 9px; color: #C6820E; font-weight: 600; margin-bottom: 6px;">log (processor-3)</div>
<div style="font-size: 8px; color: #3D7C47; margin-bottom: 8px;">COMPLETED - 12ms</div>
<div style="display: flex; flex-direction: column; gap: 2px; font-size: 8px; color: #9C9184; border-bottom: 1px solid #3a3530; padding-bottom: 4px; margin-bottom: 6px;">
<div style="display: flex; gap: 6px;">
<span style="color: #C6820E; font-weight: 600;">Input</span>
<span>Output</span>
<span>Headers</span>
</div>
</div>
<div style="font-family: monospace; font-size: 7px; color: #9C9184; line-height: 1.4;">
<div>{"orderId": "ORD-1234",</div>
<div>&nbsp;"product": "Widget A",</div>
<div>&nbsp;"quantity": 5,</div>
<div>&nbsp;"price": 29.99}</div>
</div>
</div>
</div>
</div>
</div>
<div class="card-body">
<h3>B: Left/Right Split</h3>
<p>Diagram on left, collapsible detail panel on right. Slide-in when node selected. Diagram keeps full height.</p>
<div class="pros-cons">
<div class="pros"><h4>Pros</h4><ul><li>Full diagram height</li><li>Panel can collapse</li><li>Good for wide screens</li></ul></div>
<div class="cons"><h4>Cons</h4><ul><li>Steals diagram width</li><li>Tight on narrow screens</li></ul></div>
</div>
</div>
</div>
<!-- Option C: Hybrid -->
<div class="card" data-choice="c" onclick="toggleSelect(this)">
<div class="card-image">
<div style="padding: 16px; background: #1a1612; border-radius: 6px;">
<div style="display: flex; flex-direction: column; gap: 8px; height: 280px;">
<!-- Top: Full width diagram -->
<div style="flex: 1; background: #2a2520; border-radius: 4px; padding: 12px; position: relative; overflow: hidden;">
<div style="font-size: 10px; color: #9C9184; margin-bottom: 8px;">DIAGRAM</div>
<div style="display: flex; align-items: center; gap: 6px; margin-left: 20px;">
<div style="width: 60px; height: 28px; background: #1A7F8E; border-radius: 3px; display: flex; align-items: center; justify-content: center; font-size: 8px; color: white;">from:jms</div>
<div style="width: 20px; height: 1px; background: #5C5347;"></div>
<div style="width: 60px; height: 28px; background: #C6820E; border-radius: 3px; display: flex; align-items: center; justify-content: center; font-size: 8px; color: white; border: 2px solid #3D7C47;">log</div>
<div style="width: 20px; height: 1px; background: #5C5347;"></div>
<div style="width: 60px; height: 28px; background: #C0392B; border-radius: 3px; display: flex; align-items: center; justify-content: center; font-size: 8px; color: white; border: 2px solid #C0392B;">bean</div>
<div style="width: 20px; height: 1px; background: #5C5347;"></div>
<div style="width: 60px; height: 28px; background: #3D7C47; border-radius: 3px; display: flex; align-items: center; justify-content: center; font-size: 8px; color: white; opacity: 0.4;">to:http</div>
</div>
<div style="position: absolute; bottom: 6px; right: 6px; font-size: 8px; color: #5C5347; background: #2a2520; padding: 2px 6px; border: 1px solid #3a3530; border-radius: 3px;">100%</div>
</div>
<!-- Bottom: Two-column detail -->
<div style="height: 3px; background: #3a3530; border-radius: 2px;"></div>
<div style="flex: 0 0 100px; display: flex; gap: 8px;">
<!-- Left: Processor list / timeline -->
<div style="flex: 0 0 140px; background: #2a2520; border-radius: 4px; padding: 6px; overflow: hidden;">
<div style="font-size: 8px; color: #9C9184; margin-bottom: 4px; font-weight: 600;">Processors</div>
<div style="font-size: 7px; line-height: 1.8;">
<div style="color: #3D7C47; padding: 1px 4px; background: #2a2a20; border-radius: 2px;">from:jms - 2ms</div>
<div style="color: #C6820E; padding: 1px 4px; background: #3a3020; border-radius: 2px; border-left: 2px solid #C6820E;">log - 12ms</div>
<div style="color: #C0392B; padding: 1px 4px;">bean - FAILED</div>
<div style="color: #5C5347; padding: 1px 4px; opacity: 0.5;">to:http - skipped</div>
</div>
</div>
<!-- Right: Selected processor detail -->
<div style="flex: 1; background: #2a2520; border-radius: 4px; padding: 6px; overflow: hidden;">
<div style="display: flex; gap: 8px; font-size: 8px; color: #9C9184; border-bottom: 1px solid #3a3530; padding-bottom: 3px; margin-bottom: 4px;">
<span style="color: #C6820E;">Input</span>
<span>Output</span>
<span>Headers</span>
</div>
<div style="font-family: monospace; font-size: 7px; color: #9C9184; line-height: 1.4;">
<div>{"orderId": "ORD-1234",</div>
<div>&nbsp;"product": "Widget A"}</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="card-body">
<h3>C: Top/Bottom with Processor List</h3>
<p>Diagram on top, bottom split into processor list (left) + detail tabs (right). Clicking processor in list or diagram syncs selection. Most information density.</p>
<div class="pros-cons">
<div class="pros"><h4>Pros</h4><ul><li>Processor list as navigation</li><li>Full diagram width</li><li>Maximum information density</li></ul></div>
<div class="cons"><h4>Cons</h4><ul><li>More complex layout</li><li>May feel crowded</li></ul></div>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,190 @@
<h2>Execution Overlay: Visual Intensity Comparison</h2>
<p class="subtitle">How strong should the overlay tinting be?</p>
<div class="split">
<!-- Current: Subtle -->
<div class="mockup" data-choice="subtle" onclick="toggleSelect(this)">
<div class="mockup-header">Current: Subtle (border only)</div>
<div class="mockup-body" style="background: #FAFAF8; padding: 16px;">
<div style="display: flex; flex-direction: column; gap: 12px;">
<!-- OK node - border only -->
<div style="display: flex; align-items: center; gap: 10px;">
<span style="font-size: 10px; color: #9C9184; width: 70px;">Completed</span>
<div style="position: relative; width: 160px; height: 52px; background: #fff; border: 1px solid #E4DFD8; border-radius: 6px; border-left: 4px solid #3D7C47; overflow: hidden;">
<div style="height: 5px; background: #1A7F8E;"></div>
<div style="padding: 4px 8px;">
<div style="font-size: 10px; font-weight: 600; color: #1A1612;">from:jms:orders</div>
<div style="font-size: 9px; color: #9C9184;">ENDPOINT</div>
</div>
<div style="position: absolute; bottom: 2px; right: 6px; font-size: 8px; color: #3D7C47; font-weight: 500;">2ms</div>
</div>
</div>
<!-- Failed node - border only -->
<div style="display: flex; align-items: center; gap: 10px;">
<span style="font-size: 10px; color: #9C9184; width: 70px;">Failed</span>
<div style="position: relative; width: 160px; height: 52px; background: #fff; border: 2px solid #C0392B; border-radius: 6px; overflow: hidden;">
<div style="height: 5px; background: #C6820E;"></div>
<div style="padding: 4px 8px;">
<div style="font-size: 10px; font-weight: 600; color: #1A1612;">bean:validate</div>
<div style="font-size: 9px; color: #9C9184;">BEAN</div>
</div>
<div style="position: absolute; bottom: 2px; right: 6px; font-size: 8px; color: #C0392B; font-weight: 500;">120ms</div>
<div style="position: absolute; top: -6px; right: -6px; width: 16px; height: 16px; background: #C0392B; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 9px; color: white; font-weight: bold;">!</div>
</div>
</div>
<!-- Skipped node -->
<div style="display: flex; align-items: center; gap: 10px;">
<span style="font-size: 10px; color: #9C9184; width: 70px;">Skipped</span>
<div style="opacity: 0.35; width: 160px; height: 52px; background: #fff; border: 1px solid #E4DFD8; border-radius: 6px; overflow: hidden;">
<div style="height: 5px; background: #3D7C47;"></div>
<div style="padding: 4px 8px;">
<div style="font-size: 10px; font-weight: 600; color: #1A1612;">to:http:api</div>
<div style="font-size: 9px; color: #9C9184;">TO</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Proposed: Tinted backgrounds -->
<div class="mockup" data-choice="tinted" onclick="toggleSelect(this)">
<div class="mockup-header">Proposed: Tinted backgrounds</div>
<div class="mockup-body" style="background: #FAFAF8; padding: 16px;">
<div style="display: flex; flex-direction: column; gap: 12px;">
<!-- OK node - green tint -->
<div style="display: flex; align-items: center; gap: 10px;">
<span style="font-size: 10px; color: #9C9184; width: 70px;">Completed</span>
<div style="position: relative; width: 160px; height: 52px; background: #F0F9F1; border: 1.5px solid #3D7C47; border-radius: 6px; border-left: 4px solid #3D7C47; overflow: hidden;">
<div style="height: 5px; background: #1A7F8E;"></div>
<div style="padding: 4px 8px;">
<div style="font-size: 10px; font-weight: 600; color: #1A1612;">from:jms:orders</div>
<div style="font-size: 9px; color: #9C9184;">ENDPOINT</div>
</div>
<div style="position: absolute; bottom: 2px; right: 6px; font-size: 8px; color: #3D7C47; font-weight: 500;">2ms</div>
</div>
</div>
<!-- Failed node - red tint -->
<div style="display: flex; align-items: center; gap: 10px;">
<span style="font-size: 10px; color: #9C9184; width: 70px;">Failed</span>
<div style="position: relative; width: 160px; height: 52px; background: #FDF2F0; border: 2px solid #C0392B; border-radius: 6px; overflow: hidden;">
<div style="height: 5px; background: #C6820E;"></div>
<div style="padding: 4px 8px;">
<div style="font-size: 10px; font-weight: 600; color: #C0392B;">bean:validate</div>
<div style="font-size: 9px; color: #C0392B;">FAILED</div>
</div>
<div style="position: absolute; bottom: 2px; right: 6px; font-size: 8px; color: #C0392B; font-weight: 500;">120ms</div>
<div style="position: absolute; top: -6px; right: -6px; width: 16px; height: 16px; background: #C0392B; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 9px; color: white; font-weight: bold;">!</div>
</div>
</div>
<!-- Skipped node -->
<div style="display: flex; align-items: center; gap: 10px;">
<span style="font-size: 10px; color: #9C9184; width: 70px;">Skipped</span>
<div style="opacity: 0.35; width: 160px; height: 52px; background: #fff; border: 1px solid #E4DFD8; border-radius: 6px; overflow: hidden;">
<div style="height: 5px; background: #3D7C47;"></div>
<div style="padding: 4px 8px;">
<div style="font-size: 10px; font-weight: 600; color: #1A1612;">to:http:api</div>
<div style="font-size: 9px; color: #9C9184;">TO</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<h3 style="margin-top: 24px;">Full Flow Comparison</h3>
<p class="subtitle">Same route, tinted version — see how it reads at a glance</p>
<div class="mockup">
<div class="mockup-header">Tinted overlay on a full route</div>
<div class="mockup-body" style="background: #FAFAF8; padding: 20px;">
<div style="display: flex; align-items: center; gap: 0;">
<!-- from:jms (OK) -->
<div style="position: relative;">
<div style="width: 140px; height: 52px; background: #F0F9F1; border: 1.5px solid #3D7C47; border-radius: 6px; border-left: 4px solid #3D7C47; overflow: hidden;">
<div style="height: 5px; background: #1A7F8E;"></div>
<div style="padding: 4px 8px;">
<div style="font-size: 10px; font-weight: 600; color: #1A1612;">from:jms:orders</div>
<div style="font-size: 9px; color: #9C9184;">ENDPOINT</div>
</div>
</div>
<div style="position: absolute; bottom: 2px; right: 6px; font-size: 8px; color: #3D7C47; font-weight: 500;">2ms</div>
</div>
<svg width="30" height="10" style="flex-shrink:0;"><line x1="0" y1="5" x2="25" y2="5" stroke="#3D7C47" stroke-width="1.5"/><polygon points="22,2 28,5 22,8" fill="#3D7C47"/></svg>
<!-- log (OK) -->
<div style="position: relative;">
<div style="width: 140px; height: 52px; background: #F0F9F1; border: 1.5px solid #3D7C47; border-radius: 6px; border-left: 4px solid #3D7C47; overflow: hidden;">
<div style="height: 5px; background: #C6820E;"></div>
<div style="padding: 4px 8px;">
<div style="font-size: 10px; font-weight: 600; color: #1A1612;">log:incoming</div>
<div style="font-size: 9px; color: #9C9184;">LOG</div>
</div>
</div>
<div style="position: absolute; bottom: 2px; right: 6px; font-size: 8px; color: #3D7C47; font-weight: 500;">5ms</div>
</div>
<svg width="30" height="10" style="flex-shrink:0;"><line x1="0" y1="5" x2="25" y2="5" stroke="#3D7C47" stroke-width="1.5"/><polygon points="22,2 28,5 22,8" fill="#3D7C47"/></svg>
<!-- setHeader (OK) -->
<div style="position: relative;">
<div style="width: 140px; height: 52px; background: #F0F9F1; border: 1.5px solid #3D7C47; border-radius: 6px; border-left: 4px solid #3D7C47; overflow: hidden;">
<div style="height: 5px; background: #C6820E;"></div>
<div style="padding: 4px 8px;">
<div style="font-size: 10px; font-weight: 600; color: #1A1612;">setHeader:type</div>
<div style="font-size: 9px; color: #9C9184;">SET_HEADER</div>
</div>
</div>
<div style="position: absolute; bottom: 2px; right: 6px; font-size: 8px; color: #3D7C47; font-weight: 500;">1ms</div>
</div>
<svg width="30" height="10" style="flex-shrink:0;"><line x1="0" y1="5" x2="25" y2="5" stroke="#3D7C47" stroke-width="1.5"/><polygon points="22,2 28,5 22,8" fill="#3D7C47"/></svg>
<!-- bean:validate (FAILED) -->
<div style="position: relative;">
<div style="width: 140px; height: 52px; background: #FDF2F0; border: 2px solid #C0392B; border-radius: 6px; overflow: hidden;">
<div style="height: 5px; background: #C6820E;"></div>
<div style="padding: 4px 8px;">
<div style="font-size: 10px; font-weight: 600; color: #C0392B;">bean:validate</div>
<div style="font-size: 9px; color: #C0392B;">FAILED</div>
</div>
</div>
<div style="position: absolute; bottom: 2px; right: 6px; font-size: 8px; color: #C0392B; font-weight: 500;">120ms</div>
<div style="position: absolute; top: -6px; right: -6px; width: 16px; height: 16px; background: #C0392B; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 9px; color: white; font-weight: bold;">!</div>
</div>
<svg width="30" height="10" style="flex-shrink:0;"><line x1="0" y1="5" x2="25" y2="5" stroke="#9CA3AF" stroke-width="1" stroke-dasharray="3,3"/></svg>
<!-- to:http (SKIPPED) -->
<div style="opacity: 0.35;">
<div style="width: 140px; height: 52px; background: #fff; border: 1px solid #E4DFD8; border-radius: 6px; overflow: hidden;">
<div style="height: 5px; background: #3D7C47;"></div>
<div style="padding: 4px 8px;">
<div style="font-size: 10px; font-weight: 600; color: #1A1612;">to:http:api</div>
<div style="font-size: 9px; color: #9C9184;">TO</div>
</div>
</div>
</div>
<svg width="30" height="10" style="flex-shrink:0;"><line x1="0" y1="5" x2="25" y2="5" stroke="#9CA3AF" stroke-width="1" stroke-dasharray="3,3"/></svg>
<!-- to:jms (SKIPPED) -->
<div style="opacity: 0.35;">
<div style="width: 140px; height: 52px; background: #fff; border: 1px solid #E4DFD8; border-radius: 6px; overflow: hidden;">
<div style="height: 5px; background: #3D7C47;"></div>
<div style="padding: 4px 8px;">
<div style="font-size: 10px; font-weight: 600; color: #1A1612;">to:jms:result</div>
<div style="font-size: 9px; color: #9C9184;">TO</div>
</div>
</div>
</div>
</div>
<div style="margin-top: 16px; font-size: 11px; color: #5C5347;">
<strong>Note:</strong> Edges between executed nodes turn green. Edges leading to skipped nodes become dashed gray.
</div>
</div>
</div>

View File

@@ -0,0 +1,159 @@
<h2>Execution Overlay: Success + Error Markers</h2>
<p class="subtitle">Every executed node gets a status badge — green check or red exclamation</p>
<div class="mockup">
<div class="mockup-header">Full route with status markers</div>
<div class="mockup-body" style="background: #FAFAF8; padding: 20px;">
<div style="display: flex; align-items: center; gap: 0;">
<!-- from:jms (OK) -->
<div style="position: relative;">
<div style="width: 140px; height: 52px; background: #F0F9F1; border: 1.5px solid #3D7C47; border-radius: 6px; border-left: 4px solid #3D7C47; overflow: hidden;">
<div style="height: 5px; background: #1A7F8E;"></div>
<div style="padding: 4px 8px;">
<div style="font-size: 10px; font-weight: 600; color: #1A1612;">from:jms:orders</div>
<div style="font-size: 9px; color: #9C9184;">ENDPOINT</div>
</div>
</div>
<div style="position: absolute; bottom: 2px; right: 6px; font-size: 8px; color: #3D7C47; font-weight: 500;">2ms</div>
<!-- Success marker -->
<div style="position: absolute; top: -6px; right: -6px; width: 16px; height: 16px; background: #3D7C47; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 10px; color: white; font-weight: bold;">&#10003;</div>
</div>
<svg width="30" height="10" style="flex-shrink:0;"><line x1="0" y1="5" x2="25" y2="5" stroke="#3D7C47" stroke-width="1.5"/><polygon points="22,2 28,5 22,8" fill="#3D7C47"/></svg>
<!-- log (OK) -->
<div style="position: relative;">
<div style="width: 140px; height: 52px; background: #F0F9F1; border: 1.5px solid #3D7C47; border-radius: 6px; border-left: 4px solid #3D7C47; overflow: hidden;">
<div style="height: 5px; background: #C6820E;"></div>
<div style="padding: 4px 8px;">
<div style="font-size: 10px; font-weight: 600; color: #1A1612;">log:incoming</div>
<div style="font-size: 9px; color: #9C9184;">LOG</div>
</div>
</div>
<div style="position: absolute; bottom: 2px; right: 6px; font-size: 8px; color: #3D7C47; font-weight: 500;">5ms</div>
<div style="position: absolute; top: -6px; right: -6px; width: 16px; height: 16px; background: #3D7C47; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 10px; color: white; font-weight: bold;">&#10003;</div>
</div>
<svg width="30" height="10" style="flex-shrink:0;"><line x1="0" y1="5" x2="25" y2="5" stroke="#3D7C47" stroke-width="1.5"/><polygon points="22,2 28,5 22,8" fill="#3D7C47"/></svg>
<!-- setHeader (OK) -->
<div style="position: relative;">
<div style="width: 140px; height: 52px; background: #F0F9F1; border: 1.5px solid #3D7C47; border-radius: 6px; border-left: 4px solid #3D7C47; overflow: hidden;">
<div style="height: 5px; background: #C6820E;"></div>
<div style="padding: 4px 8px;">
<div style="font-size: 10px; font-weight: 600; color: #1A1612;">setHeader:type</div>
<div style="font-size: 9px; color: #9C9184;">SET_HEADER</div>
</div>
</div>
<div style="position: absolute; bottom: 2px; right: 6px; font-size: 8px; color: #3D7C47; font-weight: 500;">1ms</div>
<div style="position: absolute; top: -6px; right: -6px; width: 16px; height: 16px; background: #3D7C47; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 10px; color: white; font-weight: bold;">&#10003;</div>
</div>
<svg width="30" height="10" style="flex-shrink:0;"><line x1="0" y1="5" x2="25" y2="5" stroke="#3D7C47" stroke-width="1.5"/><polygon points="22,2 28,5 22,8" fill="#3D7C47"/></svg>
<!-- bean:validate (FAILED) -->
<div style="position: relative;">
<div style="width: 140px; height: 52px; background: #FDF2F0; border: 2px solid #C0392B; border-radius: 6px; overflow: hidden;">
<div style="height: 5px; background: #C6820E;"></div>
<div style="padding: 4px 8px;">
<div style="font-size: 10px; font-weight: 600; color: #C0392B;">bean:validate</div>
<div style="font-size: 9px; color: #C0392B;">FAILED</div>
</div>
</div>
<div style="position: absolute; bottom: 2px; right: 6px; font-size: 8px; color: #C0392B; font-weight: 500;">120ms</div>
<!-- Error marker -->
<div style="position: absolute; top: -6px; right: -6px; width: 16px; height: 16px; background: #C0392B; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 9px; color: white; font-weight: bold;">!</div>
</div>
<svg width="30" height="10" style="flex-shrink:0;"><line x1="0" y1="5" x2="25" y2="5" stroke="#9CA3AF" stroke-width="1" stroke-dasharray="3,3"/></svg>
<!-- to:http (SKIPPED) -->
<div style="opacity: 0.35;">
<div style="width: 140px; height: 52px; background: #fff; border: 1px solid #E4DFD8; border-radius: 6px; overflow: hidden;">
<div style="height: 5px; background: #3D7C47;"></div>
<div style="padding: 4px 8px;">
<div style="font-size: 10px; font-weight: 600; color: #1A1612;">to:http:api</div>
<div style="font-size: 9px; color: #9C9184;">TO</div>
</div>
</div>
</div>
<svg width="30" height="10" style="flex-shrink:0;"><line x1="0" y1="5" x2="25" y2="5" stroke="#9CA3AF" stroke-width="1" stroke-dasharray="3,3"/></svg>
<!-- to:jms (SKIPPED) -->
<div style="opacity: 0.35;">
<div style="width: 140px; height: 52px; background: #fff; border: 1px solid #E4DFD8; border-radius: 6px; overflow: hidden;">
<div style="height: 5px; background: #3D7C47;"></div>
<div style="padding: 4px 8px;">
<div style="font-size: 10px; font-weight: 600; color: #1A1612;">to:jms:result</div>
<div style="font-size: 9px; color: #9C9184;">TO</div>
</div>
</div>
</div>
</div>
</div>
</div>
<h3 style="margin-top: 24px;">Node State Legend</h3>
<div style="display: flex; gap: 16px; flex-wrap: wrap; margin-top: 8px;">
<!-- Completed -->
<div style="display: flex; align-items: center; gap: 8px; background: #f8f8f6; padding: 10px 14px; border-radius: 6px;">
<div style="position: relative; width: 80px; height: 36px; background: #F0F9F1; border: 1.5px solid #3D7C47; border-radius: 4px; border-left: 3px solid #3D7C47;">
<div style="position: absolute; top: -5px; right: -5px; width: 14px; height: 14px; background: #3D7C47; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 8px; color: white;">&#10003;</div>
<div style="position: absolute; bottom: 1px; right: 4px; font-size: 7px; color: #3D7C47;">5ms</div>
</div>
<div>
<div style="font-size: 12px; font-weight: 600; color: #3D7C47;">Completed</div>
<div style="font-size: 10px; color: #9C9184;">Green tint + border + check badge + duration</div>
</div>
</div>
<!-- Failed -->
<div style="display: flex; align-items: center; gap: 8px; background: #f8f8f6; padding: 10px 14px; border-radius: 6px;">
<div style="position: relative; width: 80px; height: 36px; background: #FDF2F0; border: 2px solid #C0392B; border-radius: 4px;">
<div style="position: absolute; top: -5px; right: -5px; width: 14px; height: 14px; background: #C0392B; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 8px; color: white; font-weight: bold;">!</div>
<div style="position: absolute; bottom: 1px; right: 4px; font-size: 7px; color: #C0392B;">120ms</div>
</div>
<div>
<div style="font-size: 12px; font-weight: 600; color: #C0392B;">Failed</div>
<div style="font-size: 10px; color: #9C9184;">Red tint + border + ! badge + duration</div>
</div>
</div>
<!-- Sub-route failure -->
<div style="display: flex; align-items: center; gap: 8px; background: #f8f8f6; padding: 10px 14px; border-radius: 6px;">
<div style="position: relative; width: 80px; height: 36px; background: #FDF2F0; border: 2px solid #C0392B; border-radius: 4px;">
<div style="position: absolute; top: -5px; right: -5px; width: 14px; height: 14px; background: #C0392B; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 8px; color: white; font-weight: bold;">!</div>
<div style="position: absolute; bottom: 1px; left: 4px; font-size: 8px; color: #C0392B;">&#8628;</div>
<div style="position: absolute; bottom: 1px; right: 4px; font-size: 7px; color: #C0392B;">85ms</div>
</div>
<div>
<div style="font-size: 12px; font-weight: 600; color: #C0392B;">Sub-route Failure</div>
<div style="font-size: 10px; color: #9C9184;">Same as failed + drill-down arrow</div>
</div>
</div>
<!-- Skipped -->
<div style="display: flex; align-items: center; gap: 8px; background: #f8f8f6; padding: 10px 14px; border-radius: 6px;">
<div style="opacity: 0.35; width: 80px; height: 36px; background: #fff; border: 1px solid #E4DFD8; border-radius: 4px;">
</div>
<div>
<div style="font-size: 12px; font-weight: 600; color: #9C9184;">Skipped</div>
<div style="font-size: 10px; color: #9C9184;">35% opacity, no badge, no duration</div>
</div>
</div>
</div>
<h3 style="margin-top: 24px;">Edge States</h3>
<div style="display: flex; gap: 16px; flex-wrap: wrap; margin-top: 8px;">
<div style="display: flex; align-items: center; gap: 8px; background: #f8f8f6; padding: 10px 14px; border-radius: 6px;">
<svg width="60" height="10"><line x1="0" y1="5" x2="50" y2="5" stroke="#3D7C47" stroke-width="1.5"/><polygon points="47,2 53,5 47,8" fill="#3D7C47"/></svg>
<div style="font-size: 11px; color: #5C5347;"><strong>Traversed</strong> — green, solid</div>
</div>
<div style="display: flex; align-items: center; gap: 8px; background: #f8f8f6; padding: 10px 14px; border-radius: 6px;">
<svg width="60" height="10"><line x1="0" y1="5" x2="50" y2="5" stroke="#9CA3AF" stroke-width="1" stroke-dasharray="3,3"/><polygon points="47,2 53,5 47,8" fill="#9CA3AF"/></svg>
<div style="font-size: 11px; color: #5C5347;"><strong>Not traversed</strong> — gray, dashed</div>
</div>
</div>

View File

@@ -0,0 +1,3 @@
<div style="display:flex;align-items:center;justify-content:center;min-height:60vh">
<p class="subtitle">Continuing in terminal...</p>
</div>

View File

@@ -0,0 +1,181 @@
<h2>AppConfigDetailPage — New Sections</h2>
<p class="subtitle">Taps overview, route recording map, and compress success toggle added to existing config page</p>
<div class="mockup">
<div class="mockup-header">AppConfigDetailPage — Full Layout (scrollable)</div>
<div class="mockup-body" style="background:#1a1a2e;color:#e0e0e0;padding:20px;font-family:system-ui,-apple-system,sans-serif;font-size:13px;">
<!-- Back + Header -->
<div style="display:flex;align-items:center;gap:8px;margin-bottom:16px;">
<span style="color:#9ca3af;cursor:pointer;font-size:16px;">&#8592;</span>
<span style="font-size:16px;font-weight:600;">order-service</span>
<span style="font-family:monospace;font-size:11px;color:#6b7280;margin-left:8px;">v14 · Updated 3 min ago</span>
<div style="margin-left:auto;display:flex;gap:8px;">
<span style="color:#9ca3af;cursor:pointer;font-size:14px;" title="Edit">&#9998;</span>
</div>
</div>
<!-- ═══ EXISTING: Logging Section ═══ -->
<div style="background:#1e1e3a;border:1px solid #2d2d50;border-radius:8px;padding:16px;margin-bottom:12px;">
<div style="font-size:12px;font-weight:600;margin-bottom:10px;color:#9ca3af;text-transform:uppercase;letter-spacing:0.5px;">Logging</div>
<div style="display:flex;gap:24px;">
<div>
<div style="font-size:11px;color:#6b7280;margin-bottom:3px;">Log Forwarding Level</div>
<span style="background:#1e3a5f;color:#7dd3fc;padding:2px 10px;border-radius:4px;font-size:11px;">INFO</span>
</div>
</div>
</div>
<!-- ═══ EXISTING: Observability Section ═══ -->
<div style="background:#1e1e3a;border:1px solid #2d2d50;border-radius:8px;padding:16px;margin-bottom:12px;">
<div style="font-size:12px;font-weight:600;margin-bottom:10px;color:#9ca3af;text-transform:uppercase;letter-spacing:0.5px;">Observability</div>
<div style="display:flex;gap:24px;flex-wrap:wrap;">
<div>
<div style="font-size:11px;color:#6b7280;margin-bottom:3px;">Engine Level</div>
<span style="background:#1e3a5f;color:#7dd3fc;padding:2px 10px;border-radius:4px;font-size:11px;">REGULAR</span>
</div>
<div>
<div style="font-size:11px;color:#6b7280;margin-bottom:3px;">Payload Capture</div>
<span style="background:#2d1f3b;color:#d8b4fe;padding:2px 10px;border-radius:4px;font-size:11px;">BOTH</span>
</div>
<div>
<div style="font-size:11px;color:#6b7280;margin-bottom:3px;">Metrics</div>
<span style="background:#1a3a2a;color:#86efac;padding:2px 10px;border-radius:4px;font-size:11px;">ON</span>
</div>
<div>
<div style="font-size:11px;color:#6b7280;margin-bottom:3px;">Sampling Rate</div>
<span style="font-family:monospace;font-size:12px;color:#e0e0e0;">1.0</span>
</div>
<div>
<div style="font-size:11px;color:#6b7280;margin-bottom:3px;">Compress Success</div>
<span style="background:#3b2f1f;color:#fcd34d;padding:2px 10px;border-radius:4px;font-size:11px;">OFF</span>
</div>
</div>
</div>
<!-- ═══ EXISTING: Traced Processors ═══ -->
<div style="background:#1e1e3a;border:1px solid #2d2d50;border-radius:8px;padding:16px;margin-bottom:12px;">
<div style="font-size:12px;font-weight:600;margin-bottom:10px;color:#9ca3af;text-transform:uppercase;letter-spacing:0.5px;">Traced Processors</div>
<div style="font-size:11px;color:#6b7280;margin-bottom:8px;">2 processors with custom capture modes</div>
<table style="width:100%;border-collapse:collapse;font-size:12px;">
<thead>
<tr style="border-bottom:1px solid #2d2d50;">
<th style="text-align:left;padding:6px 8px;color:#9ca3af;font-size:11px;font-weight:500;">Processor ID</th>
<th style="text-align:left;padding:6px 8px;color:#9ca3af;font-size:11px;font-weight:500;">Capture Mode</th>
</tr>
</thead>
<tbody>
<tr style="border-bottom:1px solid #161630;">
<td style="padding:6px 8px;font-family:monospace;font-size:11px;">unmarshal1</td>
<td style="padding:6px 8px;"><span style="background:#2d1f3b;color:#d8b4fe;padding:1px 8px;border-radius:4px;font-size:10px;">BOTH</span></td>
</tr>
<tr>
<td style="padding:6px 8px;font-family:monospace;font-size:11px;">toDatabase</td>
<td style="padding:6px 8px;"><span style="background:#1e3a5f;color:#7dd3fc;padding:1px 8px;border-radius:4px;font-size:10px;">INPUT</span></td>
</tr>
</tbody>
</table>
</div>
<!-- ═══ NEW: Data Extraction Taps ═══ -->
<div style="background:#1e1e3a;border:1px solid #2d2d50;border-radius:8px;padding:16px;margin-bottom:12px;">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:10px;">
<div style="font-size:12px;font-weight:600;color:#9ca3af;text-transform:uppercase;letter-spacing:0.5px;">Data Extraction Taps</div>
<span style="font-size:11px;color:#6b7280;">3 taps · manage on route pages</span>
</div>
<table style="width:100%;border-collapse:collapse;font-size:12px;">
<thead>
<tr style="border-bottom:1px solid #2d2d50;">
<th style="text-align:left;padding:6px 8px;color:#9ca3af;font-size:11px;font-weight:500;">Attribute</th>
<th style="text-align:left;padding:6px 8px;color:#9ca3af;font-size:11px;font-weight:500;">Processor</th>
<th style="text-align:left;padding:6px 8px;color:#9ca3af;font-size:11px;font-weight:500;">Expression</th>
<th style="text-align:left;padding:6px 8px;color:#9ca3af;font-size:11px;font-weight:500;">Language</th>
<th style="text-align:center;padding:6px 8px;color:#9ca3af;font-size:11px;font-weight:500;">Enabled</th>
</tr>
</thead>
<tbody>
<tr style="border-bottom:1px solid #161630;">
<td style="padding:6px 8px;font-weight:500;">orderId</td>
<td style="padding:6px 8px;font-family:monospace;font-size:11px;color:#60a5fa;">unmarshal1</td>
<td style="padding:6px 8px;font-family:monospace;font-size:11px;"><span style="background:#161630;padding:1px 6px;border-radius:4px;">${body.orderId}</span></td>
<td style="padding:6px 8px;"><span style="background:#1e3a5f;color:#7dd3fc;padding:1px 6px;border-radius:4px;font-size:10px;">simple</span></td>
<td style="padding:6px 8px;text-align:center;"><span style="color:#4ade80;">&#10003;</span></td>
</tr>
<tr style="border-bottom:1px solid #161630;">
<td style="padding:6px 8px;font-weight:500;">customerId</td>
<td style="padding:6px 8px;font-family:monospace;font-size:11px;color:#60a5fa;">unmarshal1</td>
<td style="padding:6px 8px;font-family:monospace;font-size:11px;"><span style="background:#161630;padding:1px 6px;border-radius:4px;">${body.customer.id}</span></td>
<td style="padding:6px 8px;"><span style="background:#1e3a5f;color:#7dd3fc;padding:1px 6px;border-radius:4px;font-size:10px;">simple</span></td>
<td style="padding:6px 8px;text-align:center;"><span style="color:#4ade80;">&#10003;</span></td>
</tr>
<tr>
<td style="padding:6px 8px;font-weight:500;">orderTotal</td>
<td style="padding:6px 8px;font-family:monospace;font-size:11px;color:#60a5fa;">enrichPrice</td>
<td style="padding:6px 8px;font-family:monospace;font-size:11px;"><span style="background:#161630;padding:1px 6px;border-radius:4px;">$.total</span></td>
<td style="padding:6px 8px;"><span style="background:#3b2f1f;color:#fcd34d;padding:1px 6px;border-radius:4px;font-size:10px;">jsonpath</span></td>
<td style="padding:6px 8px;text-align:center;"><span style="color:#6b7280;">&#10007;</span></td>
</tr>
</tbody>
</table>
</div>
<!-- ═══ NEW: Route Recording ═══ -->
<div style="background:#1e1e3a;border:1px solid #2d2d50;border-radius:8px;padding:16px;margin-bottom:12px;">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:10px;">
<div style="font-size:12px;font-weight:600;color:#9ca3af;text-transform:uppercase;letter-spacing:0.5px;">Route Recording</div>
<span style="font-size:11px;color:#6b7280;">4 of 5 routes recording</span>
</div>
<table style="width:100%;border-collapse:collapse;font-size:12px;">
<thead>
<tr style="border-bottom:1px solid #2d2d50;">
<th style="text-align:left;padding:6px 8px;color:#9ca3af;font-size:11px;font-weight:500;">Route</th>
<th style="text-align:center;padding:6px 8px;color:#9ca3af;font-size:11px;font-weight:500;">Recording</th>
</tr>
</thead>
<tbody>
<tr style="border-bottom:1px solid #161630;">
<td style="padding:6px 8px;font-family:monospace;font-size:11px;">processOrder</td>
<td style="padding:6px 8px;text-align:center;">
<div style="width:32px;height:18px;background:#3b82f6;border-radius:9px;position:relative;margin:0 auto;cursor:pointer;">
<div style="width:14px;height:14px;background:white;border-radius:50%;position:absolute;top:2px;right:2px;"></div>
</div>
</td>
</tr>
<tr style="border-bottom:1px solid #161630;">
<td style="padding:6px 8px;font-family:monospace;font-size:11px;">processPayment</td>
<td style="padding:6px 8px;text-align:center;">
<div style="width:32px;height:18px;background:#3b82f6;border-radius:9px;position:relative;margin:0 auto;cursor:pointer;">
<div style="width:14px;height:14px;background:white;border-radius:50%;position:absolute;top:2px;right:2px;"></div>
</div>
</td>
</tr>
<tr style="border-bottom:1px solid #161630;">
<td style="padding:6px 8px;font-family:monospace;font-size:11px;">sendNotification</td>
<td style="padding:6px 8px;text-align:center;">
<div style="width:32px;height:18px;background:#3b82f6;border-radius:9px;position:relative;margin:0 auto;cursor:pointer;">
<div style="width:14px;height:14px;background:white;border-radius:50%;position:absolute;top:2px;right:2px;"></div>
</div>
</td>
</tr>
<tr style="border-bottom:1px solid #161630;">
<td style="padding:6px 8px;font-family:monospace;font-size:11px;">handleRefund</td>
<td style="padding:6px 8px;text-align:center;">
<div style="width:32px;height:18px;background:#4b5563;border-radius:9px;position:relative;margin:0 auto;cursor:pointer;">
<div style="width:14px;height:14px;background:white;border-radius:50%;position:absolute;top:2px;left:2px;"></div>
</div>
</td>
</tr>
<tr>
<td style="padding:6px 8px;font-family:monospace;font-size:11px;">healthCheck</td>
<td style="padding:6px 8px;text-align:center;">
<div style="width:32px;height:18px;background:#3b82f6;border-radius:9px;position:relative;margin:0 auto;cursor:pointer;">
<div style="width:14px;height:14px;background:white;border-radius:50%;position:absolute;top:2px;right:2px;"></div>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>

View File

@@ -0,0 +1,149 @@
<h2>AppConfigDetailPage — Final Layout</h2>
<p class="subtitle">Three clean sections: Settings, Traces & Taps, Route Recording</p>
<div class="mockup">
<div class="mockup-header">AppConfigDetailPage — Complete</div>
<div class="mockup-body" style="background:#1a1a2e;color:#e0e0e0;padding:20px;font-family:system-ui,-apple-system,sans-serif;font-size:13px;">
<!-- Back + Header -->
<div style="display:flex;align-items:center;gap:8px;margin-bottom:16px;">
<span style="color:#9ca3af;cursor:pointer;font-size:16px;">&#8592;</span>
<span style="font-size:16px;font-weight:600;">order-service</span>
<span style="font-family:monospace;font-size:11px;color:#6b7280;margin-left:8px;">v14 · Updated 3 min ago</span>
<div style="margin-left:auto;display:flex;gap:8px;">
<span style="color:#9ca3af;cursor:pointer;font-size:14px;" title="Edit">&#9998;</span>
</div>
</div>
<!-- ═══ Section 1: Settings ═══ -->
<div style="background:#1e1e3a;border:1px solid #2d2d50;border-radius:8px;padding:16px;margin-bottom:12px;">
<div style="font-size:12px;font-weight:600;margin-bottom:12px;color:#9ca3af;text-transform:uppercase;letter-spacing:0.5px;">Settings</div>
<div style="display:flex;gap:28px;flex-wrap:wrap;">
<div>
<div style="font-size:11px;color:#6b7280;margin-bottom:3px;">Log Forwarding</div>
<span style="background:#1e3a5f;color:#7dd3fc;padding:2px 10px;border-radius:4px;font-size:11px;">INFO</span>
</div>
<div>
<div style="font-size:11px;color:#6b7280;margin-bottom:3px;">Engine Level</div>
<span style="background:#1e3a5f;color:#7dd3fc;padding:2px 10px;border-radius:4px;font-size:11px;">REGULAR</span>
</div>
<div>
<div style="font-size:11px;color:#6b7280;margin-bottom:3px;">Payload Capture</div>
<span style="background:#2d1f3b;color:#d8b4fe;padding:2px 10px;border-radius:4px;font-size:11px;">BOTH</span>
</div>
<div>
<div style="font-size:11px;color:#6b7280;margin-bottom:3px;">Metrics</div>
<span style="background:#1a3a2a;color:#86efac;padding:2px 10px;border-radius:4px;font-size:11px;">ON</span>
</div>
<div>
<div style="font-size:11px;color:#6b7280;margin-bottom:3px;">Sampling Rate</div>
<span style="font-family:monospace;font-size:12px;color:#e0e0e0;">1.0</span>
</div>
<div>
<div style="font-size:11px;color:#6b7280;margin-bottom:3px;">Compress Success</div>
<span style="background:#3b2f1f;color:#fcd34d;padding:2px 10px;border-radius:4px;font-size:11px;">OFF</span>
</div>
</div>
</div>
<!-- ═══ Section 2: Traces & Taps ═══ -->
<div style="background:#1e1e3a;border:1px solid #2d2d50;border-radius:8px;padding:16px;margin-bottom:12px;">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:10px;">
<div style="font-size:12px;font-weight:600;color:#9ca3af;text-transform:uppercase;letter-spacing:0.5px;">Traces & Taps</div>
<span style="font-size:11px;color:#6b7280;">2 traced · 3 taps · manage taps on route pages</span>
</div>
<table style="width:100%;border-collapse:collapse;font-size:12px;">
<thead>
<tr style="border-bottom:1px solid #2d2d50;">
<th style="text-align:left;padding:8px;color:#9ca3af;font-size:11px;font-weight:500;">Processor</th>
<th style="text-align:left;padding:8px;color:#9ca3af;font-size:11px;font-weight:500;">Capture</th>
<th style="text-align:left;padding:8px;color:#9ca3af;font-size:11px;font-weight:500;">Taps</th>
</tr>
</thead>
<tbody>
<tr style="border-bottom:1px solid #161630;">
<td style="padding:8px;font-family:monospace;font-size:11px;color:#60a5fa;">unmarshal1</td>
<td style="padding:8px;"><span style="background:#2d1f3b;color:#d8b4fe;padding:1px 8px;border-radius:4px;font-size:10px;">BOTH</span></td>
<td style="padding:8px;">
<div style="display:flex;gap:6px;flex-wrap:wrap;">
<span style="background:#1e3a5f;color:#7dd3fc;padding:1px 8px;border-radius:10px;font-size:10px;font-family:monospace;">orderId <span style="color:#4ade80;margin-left:2px;">&#10003;</span></span>
<span style="background:#1e3a5f;color:#7dd3fc;padding:1px 8px;border-radius:10px;font-size:10px;font-family:monospace;">customerId <span style="color:#4ade80;margin-left:2px;">&#10003;</span></span>
</div>
</td>
</tr>
<tr style="border-bottom:1px solid #161630;">
<td style="padding:8px;font-family:monospace;font-size:11px;color:#60a5fa;">toDatabase</td>
<td style="padding:8px;"><span style="background:#1e3a5f;color:#7dd3fc;padding:1px 8px;border-radius:4px;font-size:10px;">INPUT</span></td>
<td style="padding:8px;"><span style="color:#6b7280;font-size:11px;"></span></td>
</tr>
<tr>
<td style="padding:8px;font-family:monospace;font-size:11px;color:#60a5fa;">enrichPrice</td>
<td style="padding:8px;"><span style="color:#6b7280;font-size:11px;"></span></td>
<td style="padding:8px;">
<span style="background:#3b2f1f;color:#fcd34d;padding:1px 8px;border-radius:10px;font-size:10px;font-family:monospace;">orderTotal <span style="color:#6b7280;margin-left:2px;">&#10007;</span></span>
</td>
</tr>
</tbody>
</table>
</div>
<!-- ═══ Section 3: Route Recording ═══ -->
<div style="background:#1e1e3a;border:1px solid #2d2d50;border-radius:8px;padding:16px;">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:10px;">
<div style="font-size:12px;font-weight:600;color:#9ca3af;text-transform:uppercase;letter-spacing:0.5px;">Route Recording</div>
<span style="font-size:11px;color:#6b7280;">4 of 5 routes recording</span>
</div>
<table style="width:100%;border-collapse:collapse;font-size:12px;">
<thead>
<tr style="border-bottom:1px solid #2d2d50;">
<th style="text-align:left;padding:6px 8px;color:#9ca3af;font-size:11px;font-weight:500;">Route</th>
<th style="text-align:center;padding:6px 8px;color:#9ca3af;font-size:11px;font-weight:500;width:80px;">Recording</th>
</tr>
</thead>
<tbody>
<tr style="border-bottom:1px solid #161630;">
<td style="padding:6px 8px;font-family:monospace;font-size:11px;">processOrder</td>
<td style="padding:6px 8px;text-align:center;">
<div style="width:32px;height:18px;background:#3b82f6;border-radius:9px;position:relative;margin:0 auto;">
<div style="width:14px;height:14px;background:white;border-radius:50%;position:absolute;top:2px;right:2px;"></div>
</div>
</td>
</tr>
<tr style="border-bottom:1px solid #161630;">
<td style="padding:6px 8px;font-family:monospace;font-size:11px;">processPayment</td>
<td style="padding:6px 8px;text-align:center;">
<div style="width:32px;height:18px;background:#3b82f6;border-radius:9px;position:relative;margin:0 auto;">
<div style="width:14px;height:14px;background:white;border-radius:50%;position:absolute;top:2px;right:2px;"></div>
</div>
</td>
</tr>
<tr style="border-bottom:1px solid #161630;">
<td style="padding:6px 8px;font-family:monospace;font-size:11px;">sendNotification</td>
<td style="padding:6px 8px;text-align:center;">
<div style="width:32px;height:18px;background:#3b82f6;border-radius:9px;position:relative;margin:0 auto;">
<div style="width:14px;height:14px;background:white;border-radius:50%;position:absolute;top:2px;right:2px;"></div>
</div>
</td>
</tr>
<tr style="border-bottom:1px solid #161630;">
<td style="padding:6px 8px;font-family:monospace;font-size:11px;">handleRefund</td>
<td style="padding:6px 8px;text-align:center;">
<div style="width:32px;height:18px;background:#4b5563;border-radius:9px;position:relative;margin:0 auto;">
<div style="width:14px;height:14px;background:white;border-radius:50%;position:absolute;top:2px;left:2px;"></div>
</div>
</td>
</tr>
<tr>
<td style="padding:6px 8px;font-family:monospace;font-size:11px;">healthCheck</td>
<td style="padding:6px 8px;text-align:center;">
<div style="width:32px;height:18px;background:#3b82f6;border-radius:9px;position:relative;margin:0 auto;">
<div style="width:14px;height:14px;background:white;border-radius:50%;position:absolute;top:2px;right:2px;"></div>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>

View File

@@ -0,0 +1,77 @@
<h2>AppConfigDetailPage — Merged "Traces & Taps" Section</h2>
<p class="subtitle">Single table combining traced processors and data extraction taps</p>
<div class="mockup">
<div class="mockup-header">Traces & Taps — Merged Table</div>
<div class="mockup-body" style="background:#1a1a2e;color:#e0e0e0;padding:20px;font-family:system-ui,-apple-system,sans-serif;font-size:13px;">
<div style="background:#1e1e3a;border:1px solid #2d2d50;border-radius:8px;padding:16px;">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:10px;">
<div style="font-size:12px;font-weight:600;color:#9ca3af;text-transform:uppercase;letter-spacing:0.5px;">Traces & Taps</div>
<span style="font-size:11px;color:#6b7280;">2 traced · 3 taps · manage taps on route pages</span>
</div>
<table style="width:100%;border-collapse:collapse;font-size:12px;">
<thead>
<tr style="border-bottom:1px solid #2d2d50;">
<th style="text-align:left;padding:8px;color:#9ca3af;font-size:11px;font-weight:500;">Processor</th>
<th style="text-align:left;padding:8px;color:#9ca3af;font-size:11px;font-weight:500;">Capture</th>
<th style="text-align:left;padding:8px;color:#9ca3af;font-size:11px;font-weight:500;">Taps</th>
</tr>
</thead>
<tbody>
<!-- Processor with both trace + taps -->
<tr style="border-bottom:1px solid #161630;">
<td style="padding:8px;font-family:monospace;font-size:11px;color:#60a5fa;">unmarshal1</td>
<td style="padding:8px;">
<span style="background:#2d1f3b;color:#d8b4fe;padding:1px 8px;border-radius:4px;font-size:10px;">BOTH</span>
</td>
<td style="padding:8px;">
<div style="display:flex;gap:6px;flex-wrap:wrap;">
<span style="background:#1e3a5f;color:#7dd3fc;padding:1px 8px;border-radius:10px;font-size:10px;font-family:monospace;">orderId <span style="color:#4ade80;margin-left:2px;">&#10003;</span></span>
<span style="background:#1e3a5f;color:#7dd3fc;padding:1px 8px;border-radius:10px;font-size:10px;font-family:monospace;">customerId <span style="color:#4ade80;margin-left:2px;">&#10003;</span></span>
</div>
</td>
</tr>
<!-- Processor with trace only -->
<tr style="border-bottom:1px solid #161630;">
<td style="padding:8px;font-family:monospace;font-size:11px;color:#60a5fa;">toDatabase</td>
<td style="padding:8px;">
<span style="background:#1e3a5f;color:#7dd3fc;padding:1px 8px;border-radius:4px;font-size:10px;">INPUT</span>
</td>
<td style="padding:8px;">
<span style="color:#6b7280;font-size:11px;"></span>
</td>
</tr>
<!-- Processor with tap only (no trace override) -->
<tr>
<td style="padding:8px;font-family:monospace;font-size:11px;color:#60a5fa;">enrichPrice</td>
<td style="padding:8px;">
<span style="color:#6b7280;font-size:11px;"></span>
</td>
<td style="padding:8px;">
<div style="display:flex;gap:6px;flex-wrap:wrap;">
<span style="background:#3b2f1f;color:#fcd34d;padding:1px 8px;border-radius:10px;font-size:10px;font-family:monospace;">orderTotal <span style="color:#6b7280;margin-left:2px;">&#10007;</span></span>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<div style="margin-top:16px;"></div>
<div class="section">
<h3>Design Notes</h3>
<ul style="font-size:14px;line-height:1.8;">
<li><strong>One row per processor</strong> that has either a capture override or taps (or both)</li>
<li><strong>Capture column:</strong> shows the trace capture mode badge, or em-dash if default</li>
<li><strong>Taps column:</strong> attribute name badges with enabled/disabled indicator (&#10003; / &#10007;), or em-dash if none</li>
<li><strong>Tap badges color-coded by language:</strong> blue = simple, yellow = jsonpath (matches RouteDetail tap table)</li>
<li><strong>Edit mode:</strong> capture column becomes a dropdown, taps remain read-only (manage on route pages)</li>
<li><strong>Empty state:</strong> "No processor-specific traces or taps configured" with link to route pages</li>
</ul>
</div>

View File

@@ -0,0 +1,150 @@
<h2>ExchangeDetail — Business Attributes & Replay</h2>
<p class="subtitle">New elements added to the existing exchange detail page</p>
<div class="mockup">
<div class="mockup-header">Exchange Detail Page — Header Card (enhanced)</div>
<div class="mockup-body" style="background:#1a1a2e;color:#e0e0e0;padding:20px;font-family:system-ui,-apple-system,sans-serif;font-size:13px;">
<!-- Exchange Header -->
<div style="display:flex;justify-content:space-between;align-items:flex-start;margin-bottom:16px;">
<div>
<div style="display:flex;align-items:center;gap:10px;margin-bottom:8px;">
<span style="width:10px;height:10px;border-radius:50%;background:#4ade80;display:inline-block;"></span>
<span style="font-family:monospace;font-size:15px;font-weight:600;">a1b2c3d4-e5f6-7890-abcd-ef1234567890</span>
<span style="background:#065f46;color:#6ee7b7;padding:2px 8px;border-radius:4px;font-size:11px;font-weight:600;">COMPLETED</span>
</div>
<div style="display:flex;gap:16px;font-size:12px;color:#9ca3af;">
<span>Route: <span style="color:#60a5fa;">processOrder</span></span>
<span>App: <span style="font-family:monospace;">order-service</span></span>
<span>Correlation: <span style="font-family:monospace;">corr-abc123</span></span>
</div>
</div>
<div style="display:flex;gap:8px;align-items:center;">
<!-- REPLAY BUTTON (NEW) -->
<button style="background:#3b82f6;color:white;border:none;padding:6px 14px;border-radius:6px;font-size:12px;font-weight:600;cursor:pointer;display:flex;align-items:center;gap:6px;">
&#x21bb; Replay
</button>
</div>
</div>
<!-- Business Attributes Strip (NEW) -->
<div style="display:flex;gap:8px;flex-wrap:wrap;padding:10px 14px;background:#1e1e3a;border:1px solid #2d2d50;border-radius:8px;margin-bottom:16px;">
<span style="font-size:11px;color:#9ca3af;margin-right:4px;line-height:24px;">Attributes</span>
<span style="background:#1e3a5f;color:#7dd3fc;padding:2px 10px;border-radius:12px;font-size:11px;font-family:monospace;">orderId: ORD-2024-78542</span>
<span style="background:#3b1f4b;color:#d8b4fe;padding:2px 10px;border-radius:12px;font-size:11px;font-family:monospace;">customerId: CUST-1234</span>
<span style="background:#1a3a2a;color:#86efac;padding:2px 10px;border-radius:12px;font-size:11px;font-family:monospace;">orderTotal: €149.90</span>
<span style="background:#3b2f1f;color:#fcd34d;padding:2px 10px;border-radius:12px;font-size:11px;font-family:monospace;">region: EU-WEST</span>
</div>
<!-- Stat boxes row -->
<div style="display:flex;gap:12px;">
<div style="flex:1;background:#1e1e3a;border:1px solid #2d2d50;border-radius:8px;padding:10px 14px;">
<div style="font-size:10px;color:#9ca3af;text-transform:uppercase;letter-spacing:0.5px;">Duration</div>
<div style="font-size:18px;font-weight:600;color:#4ade80;">245ms</div>
</div>
<div style="flex:1;background:#1e1e3a;border:1px solid #2d2d50;border-radius:8px;padding:10px 14px;">
<div style="font-size:10px;color:#9ca3af;text-transform:uppercase;letter-spacing:0.5px;">Agent</div>
<div style="font-size:14px;font-family:monospace;color:#e0e0e0;">order-svc-01</div>
</div>
<div style="flex:1;background:#1e1e3a;border:1px solid #2d2d50;border-radius:8px;padding:10px 14px;">
<div style="font-size:10px;color:#9ca3af;text-transform:uppercase;letter-spacing:0.5px;">Started</div>
<div style="font-size:14px;font-family:monospace;color:#e0e0e0;">14:23:45.123</div>
</div>
<div style="flex:1;background:#1e1e3a;border:1px solid #2d2d50;border-radius:8px;padding:10px 14px;">
<div style="font-size:10px;color:#9ca3af;text-transform:uppercase;letter-spacing:0.5px;">Processors</div>
<div style="font-size:18px;font-weight:600;color:#e0e0e0;">12</div>
</div>
</div>
</div>
</div>
<div style="margin-top:24px;"></div>
<div class="mockup">
<div class="mockup-header">Replay Confirmation Dialog</div>
<div class="mockup-body" style="background:#1a1a2e;color:#e0e0e0;padding:40px;display:flex;justify-content:center;">
<div style="background:#1e1e3a;border:1px solid #2d2d50;border-radius:12px;padding:24px;width:480px;box-shadow:0 20px 60px rgba(0,0,0,0.5);">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:16px;">
<span style="font-size:15px;font-weight:600;">Replay Exchange</span>
<span style="color:#9ca3af;cursor:pointer;"></span>
</div>
<div style="font-size:12px;color:#9ca3af;margin-bottom:16px;">
This will re-execute the exchange on the target agent. The original exchange data will be used as input.
</div>
<div style="margin-bottom:12px;">
<div style="font-size:11px;color:#9ca3af;margin-bottom:4px;">Original Exchange</div>
<div style="font-family:monospace;font-size:12px;background:#161630;border:1px solid #2d2d50;border-radius:6px;padding:8px 12px;">a1b2c3d4-e5f6-7890-abcd-ef1234567890</div>
</div>
<div style="margin-bottom:12px;">
<div style="font-size:11px;color:#9ca3af;margin-bottom:4px;">Target Agent</div>
<div style="background:#161630;border:1px solid #2d2d50;border-radius:6px;padding:8px 12px;font-size:12px;display:flex;justify-content:space-between;align-items:center;">
<span style="font-family:monospace;">order-svc-01</span>
<span style="color:#9ca3af;font-size:10px;"></span>
</div>
</div>
<div style="margin-bottom:16px;">
<div style="font-size:11px;color:#9ca3af;margin-bottom:4px;">Route</div>
<div style="font-family:monospace;font-size:12px;background:#161630;border:1px solid #2d2d50;border-radius:6px;padding:8px 12px;">processOrder</div>
</div>
<div style="display:flex;gap:8px;justify-content:flex-end;">
<button style="background:transparent;color:#9ca3af;border:1px solid #2d2d50;padding:6px 16px;border-radius:6px;font-size:12px;cursor:pointer;">Cancel</button>
<button style="background:#3b82f6;color:white;border:none;padding:6px 16px;border-radius:6px;font-size:12px;font-weight:600;cursor:pointer;">&#x21bb; Replay</button>
</div>
</div>
</div>
</div>
<div style="margin-top:24px;"></div>
<div class="mockup">
<div class="mockup-header">Dashboard — Exchanges Table (with business attributes)</div>
<div class="mockup-body" style="background:#1a1a2e;color:#e0e0e0;padding:16px;font-family:system-ui,-apple-system,sans-serif;font-size:12px;">
<table style="width:100%;border-collapse:collapse;">
<thead>
<tr style="border-bottom:1px solid #2d2d50;text-align:left;">
<th style="padding:8px 12px;color:#9ca3af;font-size:11px;font-weight:500;">Status</th>
<th style="padding:8px 12px;color:#9ca3af;font-size:11px;font-weight:500;">Route</th>
<th style="padding:8px 12px;color:#9ca3af;font-size:11px;font-weight:500;">App</th>
<th style="padding:8px 12px;color:#9ca3af;font-size:11px;font-weight:500;">Attributes</th>
<th style="padding:8px 12px;color:#9ca3af;font-size:11px;font-weight:500;">Exchange ID</th>
<th style="padding:8px 12px;color:#9ca3af;font-size:11px;font-weight:500;">Duration</th>
</tr>
</thead>
<tbody>
<tr style="border-bottom:1px solid #1e1e3a;">
<td style="padding:8px 12px;"><span style="width:8px;height:8px;border-radius:50%;background:#4ade80;display:inline-block;"></span> <span style="color:#6ee7b7;font-size:11px;">OK</span></td>
<td style="padding:8px 12px;color:#60a5fa;">processOrder</td>
<td style="padding:8px 12px;font-family:monospace;">order-svc</td>
<td style="padding:8px 12px;">
<span style="background:#1e3a5f;color:#7dd3fc;padding:1px 6px;border-radius:8px;font-size:10px;font-family:monospace;">ORD-78542</span>
<span style="background:#3b1f4b;color:#d8b4fe;padding:1px 6px;border-radius:8px;font-size:10px;font-family:monospace;">CUST-1234</span>
</td>
<td style="padding:8px 12px;font-family:monospace;font-size:11px;">a1b2c3d4-e5f6…</td>
<td style="padding:8px 12px;font-family:monospace;color:#4ade80;">245ms</td>
</tr>
<tr style="border-bottom:1px solid #1e1e3a;">
<td style="padding:8px 12px;"><span style="width:8px;height:8px;border-radius:50%;background:#f87171;display:inline-block;"></span> <span style="color:#fca5a5;font-size:11px;">ERR</span></td>
<td style="padding:8px 12px;color:#60a5fa;">processPayment</td>
<td style="padding:8px 12px;font-family:monospace;">payment-svc</td>
<td style="padding:8px 12px;">
<span style="background:#1e3a5f;color:#7dd3fc;padding:1px 6px;border-radius:8px;font-size:10px;font-family:monospace;">PAY-91023</span>
<span style="color:#6b7280;font-size:10px;">+2</span>
</td>
<td style="padding:8px 12px;font-family:monospace;font-size:11px;">f8e7d6c5-b4a3…</td>
<td style="padding:8px 12px;font-family:monospace;color:#f87171;">1,234ms</td>
</tr>
<tr style="border-bottom:1px solid #1e1e3a;">
<td style="padding:8px 12px;"><span style="width:8px;height:8px;border-radius:50%;background:#4ade80;display:inline-block;"></span> <span style="color:#6ee7b7;font-size:11px;">OK</span></td>
<td style="padding:8px 12px;color:#60a5fa;">sendNotification</td>
<td style="padding:8px 12px;font-family:monospace;">notif-svc</td>
<td style="padding:8px 12px;"><span style="color:#6b7280;font-size:10px;font-style:italic;"></span></td>
<td style="padding:8px 12px;font-family:monospace;font-size:11px;">12345678-abcd…</td>
<td style="padding:8px 12px;font-family:monospace;color:#4ade80;">89ms</td>
</tr>
</tbody>
</table>
<div style="margin-top:12px;font-size:11px;color:#6b7280;">
Note: Attributes column shows first 2 values as compact badges, "+N" overflow indicator when more exist. Em-dash when no attributes extracted.
</div>
</div>
</div>

View File

@@ -0,0 +1,138 @@
<h2>Replay Dialog — Revised</h2>
<p class="subtitle">Target agent selection + editable payload and headers</p>
<div class="mockup">
<div class="mockup-header">Replay Exchange Dialog (large modal)</div>
<div class="mockup-body" style="background:#1a1a2e;color:#e0e0e0;padding:40px;display:flex;justify-content:center;">
<div style="background:#1e1e3a;border:1px solid #2d2d50;border-radius:12px;padding:0;width:640px;box-shadow:0 20px 60px rgba(0,0,0,0.5);overflow:hidden;">
<!-- Dialog header -->
<div style="display:flex;justify-content:space-between;align-items:center;padding:16px 20px;border-bottom:1px solid #2d2d50;">
<span style="font-size:15px;font-weight:600;">Replay Exchange</span>
<span style="color:#9ca3af;cursor:pointer;font-size:18px;"></span>
</div>
<div style="padding:20px;">
<!-- Warning -->
<div style="font-size:12px;color:#fbbf24;background:#3b2f1f;border:1px solid #854d0e;border-radius:6px;padding:8px 12px;margin-bottom:16px;display:flex;align-items:center;gap:8px;">
<span></span> This will re-execute the exchange on the selected agent.
</div>
<!-- Target Agent -->
<div style="margin-bottom:16px;">
<div style="font-size:11px;color:#9ca3af;margin-bottom:4px;font-weight:500;">Target Agent</div>
<div style="background:#161630;border:1px solid #2d2d50;border-radius:6px;padding:8px 12px;font-size:12px;display:flex;justify-content:space-between;align-items:center;">
<span style="font-family:monospace;">order-svc-01</span>
<span style="color:#9ca3af;font-size:10px;"></span>
</div>
<div style="font-size:10px;color:#6b7280;margin-top:4px;">Only LIVE agents for this application are shown</div>
</div>
<!-- Tabs: Headers / Body -->
<div style="display:flex;gap:0;margin-bottom:0;border-bottom:1px solid #2d2d50;">
<div style="padding:8px 16px;font-size:12px;font-weight:600;color:#60a5fa;border-bottom:2px solid #3b82f6;cursor:pointer;">Headers</div>
<div style="padding:8px 16px;font-size:12px;color:#9ca3af;cursor:pointer;">Body</div>
</div>
<!-- Headers tab content -->
<div style="background:#161630;border:1px solid #2d2d50;border-top:none;border-radius:0 0 6px 6px;padding:12px;margin-bottom:16px;">
<table style="width:100%;border-collapse:collapse;font-size:11px;">
<thead>
<tr style="border-bottom:1px solid #2d2d50;">
<th style="text-align:left;padding:4px 8px;color:#9ca3af;font-weight:500;width:35%;">Key</th>
<th style="text-align:left;padding:4px 8px;color:#9ca3af;font-weight:500;">Value</th>
<th style="width:32px;"></th>
</tr>
</thead>
<tbody>
<tr style="border-bottom:1px solid #1e1e3a;">
<td style="padding:4px 8px;"><input style="background:#1a1a2e;border:1px solid #2d2d50;border-radius:4px;color:#e0e0e0;padding:4px 8px;width:100%;font-family:monospace;font-size:11px;box-sizing:border-box;" value="Content-Type" /></td>
<td style="padding:4px 8px;"><input style="background:#1a1a2e;border:1px solid #2d2d50;border-radius:4px;color:#e0e0e0;padding:4px 8px;width:100%;font-family:monospace;font-size:11px;box-sizing:border-box;" value="application/json" /></td>
<td style="padding:4px 8px;text-align:center;"><span style="color:#f87171;cursor:pointer;font-size:14px;"></span></td>
</tr>
<tr style="border-bottom:1px solid #1e1e3a;">
<td style="padding:4px 8px;"><input style="background:#1a1a2e;border:1px solid #2d2d50;border-radius:4px;color:#e0e0e0;padding:4px 8px;width:100%;font-family:monospace;font-size:11px;box-sizing:border-box;" value="X-Correlation-Id" /></td>
<td style="padding:4px 8px;"><input style="background:#1a1a2e;border:1px solid #2d2d50;border-radius:4px;color:#e0e0e0;padding:4px 8px;width:100%;font-family:monospace;font-size:11px;box-sizing:border-box;" value="corr-abc123" /></td>
<td style="padding:4px 8px;text-align:center;"><span style="color:#f87171;cursor:pointer;font-size:14px;"></span></td>
</tr>
<tr>
<td colspan="3" style="padding:6px 8px;">
<span style="color:#3b82f6;cursor:pointer;font-size:11px;">+ Add header</span>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- Footer -->
<div style="display:flex;gap:8px;justify-content:flex-end;padding:12px 20px;border-top:1px solid #2d2d50;background:#1a1a30;">
<button style="background:transparent;color:#9ca3af;border:1px solid #2d2d50;padding:6px 16px;border-radius:6px;font-size:12px;cursor:pointer;">Cancel</button>
<button style="background:#3b82f6;color:white;border:none;padding:6px 16px;border-radius:6px;font-size:12px;font-weight:600;cursor:pointer;">&#x21bb; Replay</button>
</div>
</div>
</div>
</div>
<div style="margin-top:24px;"></div>
<div class="mockup">
<div class="mockup-header">Replay Dialog — Body Tab</div>
<div class="mockup-body" style="background:#1a1a2e;color:#e0e0e0;padding:40px;display:flex;justify-content:center;">
<div style="background:#1e1e3a;border:1px solid #2d2d50;border-radius:12px;padding:0;width:640px;box-shadow:0 20px 60px rgba(0,0,0,0.5);overflow:hidden;">
<!-- Dialog header -->
<div style="display:flex;justify-content:space-between;align-items:center;padding:16px 20px;border-bottom:1px solid #2d2d50;">
<span style="font-size:15px;font-weight:600;">Replay Exchange</span>
<span style="color:#9ca3af;cursor:pointer;font-size:18px;"></span>
</div>
<div style="padding:20px;">
<!-- Warning -->
<div style="font-size:12px;color:#fbbf24;background:#3b2f1f;border:1px solid #854d0e;border-radius:6px;padding:8px 12px;margin-bottom:16px;display:flex;align-items:center;gap:8px;">
<span></span> This will re-execute the exchange on the selected agent.
</div>
<!-- Target Agent (collapsed) -->
<div style="margin-bottom:16px;">
<div style="font-size:11px;color:#9ca3af;margin-bottom:4px;font-weight:500;">Target Agent</div>
<div style="background:#161630;border:1px solid #2d2d50;border-radius:6px;padding:8px 12px;font-size:12px;display:flex;justify-content:space-between;align-items:center;">
<span style="font-family:monospace;">order-svc-01</span>
<span style="color:#9ca3af;font-size:10px;"></span>
</div>
</div>
<!-- Tabs: Headers / Body -->
<div style="display:flex;gap:0;margin-bottom:0;border-bottom:1px solid #2d2d50;">
<div style="padding:8px 16px;font-size:12px;color:#9ca3af;cursor:pointer;">Headers</div>
<div style="padding:8px 16px;font-size:12px;font-weight:600;color:#60a5fa;border-bottom:2px solid #3b82f6;cursor:pointer;">Body</div>
</div>
<!-- Body tab content — editable code area -->
<div style="background:#161630;border:1px solid #2d2d50;border-top:none;border-radius:0 0 6px 6px;padding:0;margin-bottom:16px;position:relative;">
<div style="display:flex;justify-content:flex-end;padding:6px 8px;border-bottom:1px solid #2d2d50;">
<span style="font-size:10px;color:#6b7280;background:#1a1a2e;padding:2px 8px;border-radius:4px;">JSON</span>
</div>
<pre style="margin:0;padding:12px;font-family:monospace;font-size:11px;line-height:1.6;color:#e0e0e0;min-height:160px;overflow:auto;white-space:pre;"><span style="color:#9ca3af;">{</span>
<span style="color:#7dd3fc;">"orderId"</span><span style="color:#9ca3af;">:</span> <span style="color:#fcd34d;">"ORD-2024-78542"</span><span style="color:#9ca3af;">,</span>
<span style="color:#7dd3fc;">"customerId"</span><span style="color:#9ca3af;">:</span> <span style="color:#fcd34d;">"CUST-1234"</span><span style="color:#9ca3af;">,</span>
<span style="color:#7dd3fc;">"items"</span><span style="color:#9ca3af;">:</span> <span style="color:#9ca3af;">[</span>
<span style="color:#9ca3af;">{</span>
<span style="color:#7dd3fc;">"sku"</span><span style="color:#9ca3af;">:</span> <span style="color:#fcd34d;">"WIDGET-001"</span><span style="color:#9ca3af;">,</span>
<span style="color:#7dd3fc;">"qty"</span><span style="color:#9ca3af;">:</span> <span style="color:#c4b5fd;">3</span><span style="color:#9ca3af;">,</span>
<span style="color:#7dd3fc;">"price"</span><span style="color:#9ca3af;">:</span> <span style="color:#c4b5fd;">49.97</span>
<span style="color:#9ca3af;">}</span>
<span style="color:#9ca3af;">],</span>
<span style="color:#7dd3fc;">"total"</span><span style="color:#9ca3af;">:</span> <span style="color:#c4b5fd;">149.90</span>
<span style="color:#9ca3af;">}</span></pre>
</div>
</div>
<!-- Footer -->
<div style="display:flex;gap:8px;justify-content:flex-end;padding:12px 20px;border-top:1px solid #2d2d50;background:#1a1a30;">
<button style="background:transparent;color:#9ca3af;border:1px solid #2d2d50;padding:6px 16px;border-radius:6px;font-size:12px;cursor:pointer;">Cancel</button>
<button style="background:#3b82f6;color:white;border:none;padding:6px 16px;border-radius:6px;font-size:12px;font-weight:600;cursor:pointer;">&#x21bb; Replay</button>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,221 @@
<h2>RouteDetail — Tap Management & Recording Toggle</h2>
<p class="subtitle">New "Taps" tab on RouteDetail + recording toggle in header</p>
<div class="mockup">
<div class="mockup-header">RouteDetail Page — Header with Recording Toggle</div>
<div class="mockup-body" style="background:#1a1a2e;color:#e0e0e0;padding:20px;font-family:system-ui,-apple-system,sans-serif;font-size:13px;">
<!-- Route header -->
<div style="display:flex;justify-content:space-between;align-items:flex-start;margin-bottom:16px;">
<div>
<div style="font-size:16px;font-weight:600;margin-bottom:4px;">processOrder</div>
<div style="font-size:12px;color:#9ca3af;">
<span style="font-family:monospace;">order-service</span>
<span style="margin:0 8px;color:#2d2d50;">|</span>
<span style="color:#4ade80;">99.2% success</span>
<span style="margin:0 8px;color:#2d2d50;">|</span>
<span>245ms avg</span>
</div>
</div>
<div style="display:flex;align-items:center;gap:12px;">
<!-- Recording toggle -->
<div style="display:flex;align-items:center;gap:8px;background:#1e1e3a;border:1px solid #2d2d50;border-radius:8px;padding:6px 12px;">
<span style="font-size:11px;color:#9ca3af;">Recording</span>
<div style="width:36px;height:20px;background:#3b82f6;border-radius:10px;position:relative;cursor:pointer;">
<div style="width:16px;height:16px;background:white;border-radius:50%;position:absolute;top:2px;right:2px;transition:all 0.2s;"></div>
</div>
</div>
</div>
</div>
<!-- KPI strip (abbreviated) -->
<div style="display:flex;gap:10px;margin-bottom:16px;">
<div style="flex:1;background:#1e1e3a;border:1px solid #2d2d50;border-radius:8px;padding:8px 12px;">
<div style="font-size:10px;color:#9ca3af;">Success Rate</div>
<div style="font-size:16px;font-weight:600;color:#4ade80;">99.2%</div>
</div>
<div style="flex:1;background:#1e1e3a;border:1px solid #2d2d50;border-radius:8px;padding:8px 12px;">
<div style="font-size:10px;color:#9ca3af;">Avg Duration</div>
<div style="font-size:16px;font-weight:600;">245ms</div>
</div>
<div style="flex:1;background:#1e1e3a;border:1px solid #2d2d50;border-radius:8px;padding:8px 12px;">
<div style="font-size:10px;color:#9ca3af;">Total</div>
<div style="font-size:16px;font-weight:600;">12,482</div>
</div>
<div style="flex:1;background:#1e1e3a;border:1px solid #2d2d50;border-radius:8px;padding:8px 12px;">
<div style="font-size:10px;color:#9ca3af;">Active Taps</div>
<div style="font-size:16px;font-weight:600;color:#60a5fa;">3</div>
</div>
</div>
<!-- Tabs -->
<div style="display:flex;gap:0;border-bottom:1px solid #2d2d50;margin-bottom:16px;">
<div style="padding:8px 16px;font-size:12px;color:#9ca3af;cursor:pointer;">Overview</div>
<div style="padding:8px 16px;font-size:12px;color:#9ca3af;cursor:pointer;">Processors</div>
<div style="padding:8px 16px;font-size:12px;color:#9ca3af;cursor:pointer;">Errors</div>
<div style="padding:8px 16px;font-size:12px;color:#9ca3af;cursor:pointer;">Executions</div>
<div style="padding:8px 16px;font-size:12px;font-weight:600;color:#60a5fa;border-bottom:2px solid #3b82f6;cursor:pointer;">Taps</div>
</div>
<!-- Taps tab content -->
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px;">
<div style="font-size:13px;font-weight:600;">Data Extraction Taps</div>
<button style="background:#3b82f6;color:white;border:none;padding:6px 14px;border-radius:6px;font-size:12px;font-weight:500;cursor:pointer;display:flex;align-items:center;gap:4px;">+ Add Tap</button>
</div>
<!-- Taps table -->
<div style="background:#1e1e3a;border:1px solid #2d2d50;border-radius:8px;overflow:hidden;">
<table style="width:100%;border-collapse:collapse;font-size:12px;">
<thead>
<tr style="border-bottom:1px solid #2d2d50;">
<th style="text-align:left;padding:10px 12px;color:#9ca3af;font-size:11px;font-weight:500;">Attribute</th>
<th style="text-align:left;padding:10px 12px;color:#9ca3af;font-size:11px;font-weight:500;">Processor</th>
<th style="text-align:left;padding:10px 12px;color:#9ca3af;font-size:11px;font-weight:500;">Expression</th>
<th style="text-align:left;padding:10px 12px;color:#9ca3af;font-size:11px;font-weight:500;">Language</th>
<th style="text-align:left;padding:10px 12px;color:#9ca3af;font-size:11px;font-weight:500;">Target</th>
<th style="text-align:left;padding:10px 12px;color:#9ca3af;font-size:11px;font-weight:500;">Type</th>
<th style="text-align:center;padding:10px 12px;color:#9ca3af;font-size:11px;font-weight:500;">Enabled</th>
<th style="width:60px;"></th>
</tr>
</thead>
<tbody>
<tr style="border-bottom:1px solid #161630;">
<td style="padding:8px 12px;font-weight:500;">orderId</td>
<td style="padding:8px 12px;font-family:monospace;font-size:11px;color:#60a5fa;">unmarshal1</td>
<td style="padding:8px 12px;font-family:monospace;font-size:11px;"><span style="background:#161630;padding:2px 6px;border-radius:4px;">${body.orderId}</span></td>
<td style="padding:8px 12px;"><span style="background:#1e3a5f;color:#7dd3fc;padding:1px 8px;border-radius:4px;font-size:10px;">simple</span></td>
<td style="padding:8px 12px;"><span style="background:#2d1f3b;color:#d8b4fe;padding:1px 8px;border-radius:4px;font-size:10px;">OUTPUT</span></td>
<td style="padding:8px 12px;"><span style="background:#1a3a2a;color:#86efac;padding:1px 8px;border-radius:4px;font-size:10px;">BUSINESS</span></td>
<td style="padding:8px 12px;text-align:center;">
<div style="width:32px;height:18px;background:#3b82f6;border-radius:9px;position:relative;margin:0 auto;cursor:pointer;">
<div style="width:14px;height:14px;background:white;border-radius:50%;position:absolute;top:2px;right:2px;"></div>
</div>
</td>
<td style="padding:8px 12px;text-align:center;">
<span style="color:#9ca3af;cursor:pointer;font-size:14px;" title="Edit">&#9998;</span>
<span style="color:#f87171;cursor:pointer;font-size:14px;margin-left:6px;" title="Delete">&#x2715;</span>
</td>
</tr>
<tr style="border-bottom:1px solid #161630;">
<td style="padding:8px 12px;font-weight:500;">customerId</td>
<td style="padding:8px 12px;font-family:monospace;font-size:11px;color:#60a5fa;">unmarshal1</td>
<td style="padding:8px 12px;font-family:monospace;font-size:11px;"><span style="background:#161630;padding:2px 6px;border-radius:4px;">${body.customer.id}</span></td>
<td style="padding:8px 12px;"><span style="background:#1e3a5f;color:#7dd3fc;padding:1px 8px;border-radius:4px;font-size:10px;">simple</span></td>
<td style="padding:8px 12px;"><span style="background:#2d1f3b;color:#d8b4fe;padding:1px 8px;border-radius:4px;font-size:10px;">OUTPUT</span></td>
<td style="padding:8px 12px;"><span style="background:#1a3a2a;color:#86efac;padding:1px 8px;border-radius:4px;font-size:10px;">CORRELATION</span></td>
<td style="padding:8px 12px;text-align:center;">
<div style="width:32px;height:18px;background:#3b82f6;border-radius:9px;position:relative;margin:0 auto;cursor:pointer;">
<div style="width:14px;height:14px;background:white;border-radius:50%;position:absolute;top:2px;right:2px;"></div>
</div>
</td>
<td style="padding:8px 12px;text-align:center;">
<span style="color:#9ca3af;cursor:pointer;font-size:14px;" title="Edit">&#9998;</span>
<span style="color:#f87171;cursor:pointer;font-size:14px;margin-left:6px;" title="Delete">&#x2715;</span>
</td>
</tr>
<tr>
<td style="padding:8px 12px;font-weight:500;">orderTotal</td>
<td style="padding:8px 12px;font-family:monospace;font-size:11px;color:#60a5fa;">enrichPrice</td>
<td style="padding:8px 12px;font-family:monospace;font-size:11px;"><span style="background:#161630;padding:2px 6px;border-radius:4px;">$.total</span></td>
<td style="padding:8px 12px;"><span style="background:#3b2f1f;color:#fcd34d;padding:1px 8px;border-radius:4px;font-size:10px;">jsonpath</span></td>
<td style="padding:8px 12px;"><span style="background:#2d1f3b;color:#d8b4fe;padding:1px 8px;border-radius:4px;font-size:10px;">OUTPUT</span></td>
<td style="padding:8px 12px;"><span style="background:#1a3a2a;color:#86efac;padding:1px 8px;border-radius:4px;font-size:10px;">BUSINESS</span></td>
<td style="padding:8px 12px;text-align:center;">
<div style="width:32px;height:18px;background:#4b5563;border-radius:9px;position:relative;margin:0 auto;cursor:pointer;">
<div style="width:14px;height:14px;background:white;border-radius:50%;position:absolute;top:2px;left:2px;"></div>
</div>
</td>
<td style="padding:8px 12px;text-align:center;">
<span style="color:#9ca3af;cursor:pointer;font-size:14px;" title="Edit">&#9998;</span>
<span style="color:#f87171;cursor:pointer;font-size:14px;margin-left:6px;" title="Delete">&#x2715;</span>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<div style="margin-top:24px;"></div>
<div class="mockup">
<div class="mockup-header">Add/Edit Tap — Modal Dialog</div>
<div class="mockup-body" style="background:#1a1a2e;color:#e0e0e0;padding:40px;display:flex;justify-content:center;">
<div style="background:#1e1e3a;border:1px solid #2d2d50;border-radius:12px;padding:0;width:520px;box-shadow:0 20px 60px rgba(0,0,0,0.5);overflow:hidden;">
<!-- Header -->
<div style="display:flex;justify-content:space-between;align-items:center;padding:16px 20px;border-bottom:1px solid #2d2d50;">
<span style="font-size:15px;font-weight:600;">Add Tap</span>
<span style="color:#9ca3af;cursor:pointer;font-size:18px;"></span>
</div>
<div style="padding:20px;">
<!-- Attribute Name -->
<div style="margin-bottom:14px;">
<div style="font-size:11px;color:#9ca3af;margin-bottom:4px;font-weight:500;">Attribute Name <span style="color:#f87171;">*</span></div>
<input style="background:#161630;border:1px solid #2d2d50;border-radius:6px;color:#e0e0e0;padding:8px 12px;width:100%;font-size:12px;box-sizing:border-box;" placeholder="e.g. orderId, customerId" />
</div>
<!-- Processor -->
<div style="margin-bottom:14px;">
<div style="font-size:11px;color:#9ca3af;margin-bottom:4px;font-weight:500;">Processor <span style="color:#f87171;">*</span></div>
<div style="background:#161630;border:1px solid #2d2d50;border-radius:6px;padding:8px 12px;font-size:12px;display:flex;justify-content:space-between;align-items:center;">
<span style="color:#6b7280;">Select processor…</span>
<span style="color:#9ca3af;font-size:10px;"></span>
</div>
<div style="font-size:10px;color:#6b7280;margin-top:3px;">Processors from this route's diagram</div>
</div>
<!-- Two columns: Language + Target -->
<div style="display:flex;gap:12px;margin-bottom:14px;">
<div style="flex:1;">
<div style="font-size:11px;color:#9ca3af;margin-bottom:4px;font-weight:500;">Language <span style="color:#f87171;">*</span></div>
<div style="background:#161630;border:1px solid #2d2d50;border-radius:6px;padding:8px 12px;font-size:12px;display:flex;justify-content:space-between;align-items:center;">
<span>simple</span>
<span style="color:#9ca3af;font-size:10px;"></span>
</div>
</div>
<div style="flex:1;">
<div style="font-size:11px;color:#9ca3af;margin-bottom:4px;font-weight:500;">Target <span style="color:#f87171;">*</span></div>
<div style="background:#161630;border:1px solid #2d2d50;border-radius:6px;padding:8px 12px;font-size:12px;display:flex;justify-content:space-between;align-items:center;">
<span>OUTPUT</span>
<span style="color:#9ca3af;font-size:10px;"></span>
</div>
</div>
</div>
<!-- Expression -->
<div style="margin-bottom:14px;">
<div style="font-size:11px;color:#9ca3af;margin-bottom:4px;font-weight:500;">Expression <span style="color:#f87171;">*</span></div>
<textarea style="background:#161630;border:1px solid #2d2d50;border-radius:6px;color:#e0e0e0;padding:8px 12px;width:100%;font-family:monospace;font-size:12px;box-sizing:border-box;resize:vertical;min-height:48px;" placeholder="e.g. ${body.orderId} or $.customer.id">${body.orderId}</textarea>
<div style="font-size:10px;color:#6b7280;margin-top:3px;">Camel expression — evaluated at the selected processor</div>
</div>
<!-- Attribute Type -->
<div style="margin-bottom:14px;">
<div style="font-size:11px;color:#9ca3af;margin-bottom:4px;font-weight:500;">Attribute Type</div>
<div style="display:flex;gap:8px;">
<div style="background:#1e3a5f;color:#7dd3fc;padding:4px 12px;border-radius:6px;font-size:11px;cursor:pointer;border:1px solid #3b82f6;">BUSINESS_OBJECT</div>
<div style="background:#161630;color:#9ca3af;padding:4px 12px;border-radius:6px;font-size:11px;cursor:pointer;border:1px solid #2d2d50;">CORRELATION</div>
<div style="background:#161630;color:#9ca3af;padding:4px 12px;border-radius:6px;font-size:11px;cursor:pointer;border:1px solid #2d2d50;">EVENT</div>
<div style="background:#161630;color:#9ca3af;padding:4px 12px;border-radius:6px;font-size:11px;cursor:pointer;border:1px solid #2d2d50;">CUSTOM</div>
</div>
</div>
<!-- Enabled -->
<div style="display:flex;align-items:center;gap:8px;margin-bottom:4px;">
<div style="width:36px;height:20px;background:#3b82f6;border-radius:10px;position:relative;cursor:pointer;">
<div style="width:16px;height:16px;background:white;border-radius:50%;position:absolute;top:2px;right:2px;"></div>
</div>
<span style="font-size:12px;color:#e0e0e0;">Enabled</span>
</div>
</div>
<!-- Footer -->
<div style="display:flex;gap:8px;justify-content:flex-end;padding:12px 20px;border-top:1px solid #2d2d50;background:#1a1a30;">
<button style="background:transparent;color:#9ca3af;border:1px solid #2d2d50;padding:6px 16px;border-radius:6px;font-size:12px;cursor:pointer;">Cancel</button>
<button style="background:#3b82f6;color:white;border:none;padding:6px 16px;border-radius:6px;font-size:12px;font-weight:600;cursor:pointer;">Save Tap</button>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,175 @@
<h2>Add Tap — With Expression Testing</h2>
<p class="subtitle">Collapsible test section at bottom of the tap modal</p>
<div class="mockup">
<div class="mockup-header">Add Tap Modal — Test Expression (Recent Exchange)</div>
<div class="mockup-body" style="background:#1a1a2e;color:#e0e0e0;padding:40px;display:flex;justify-content:center;">
<div style="background:#1e1e3a;border:1px solid #2d2d50;border-radius:12px;padding:0;width:560px;box-shadow:0 20px 60px rgba(0,0,0,0.5);overflow:hidden;">
<!-- Header -->
<div style="display:flex;justify-content:space-between;align-items:center;padding:16px 20px;border-bottom:1px solid #2d2d50;">
<span style="font-size:15px;font-weight:600;">Add Tap</span>
<span style="color:#9ca3af;cursor:pointer;font-size:18px;"></span>
</div>
<div style="padding:20px;max-height:70vh;overflow-y:auto;">
<!-- Form fields (collapsed for brevity) -->
<div style="margin-bottom:14px;">
<div style="font-size:11px;color:#9ca3af;margin-bottom:4px;font-weight:500;">Attribute Name <span style="color:#f87171;">*</span></div>
<input style="background:#161630;border:1px solid #2d2d50;border-radius:6px;color:#e0e0e0;padding:8px 12px;width:100%;font-size:12px;box-sizing:border-box;" value="orderId" />
</div>
<div style="margin-bottom:14px;">
<div style="font-size:11px;color:#9ca3af;margin-bottom:4px;font-weight:500;">Processor <span style="color:#f87171;">*</span></div>
<div style="background:#161630;border:1px solid #2d2d50;border-radius:6px;padding:8px 12px;font-size:12px;display:flex;justify-content:space-between;align-items:center;">
<span style="font-family:monospace;">unmarshal1</span>
<span style="color:#9ca3af;font-size:10px;"></span>
</div>
</div>
<div style="display:flex;gap:12px;margin-bottom:14px;">
<div style="flex:1;">
<div style="font-size:11px;color:#9ca3af;margin-bottom:4px;font-weight:500;">Language</div>
<div style="background:#161630;border:1px solid #2d2d50;border-radius:6px;padding:8px 12px;font-size:12px;display:flex;justify-content:space-between;align-items:center;">
<span>simple</span><span style="color:#9ca3af;font-size:10px;"></span>
</div>
</div>
<div style="flex:1;">
<div style="font-size:11px;color:#9ca3af;margin-bottom:4px;font-weight:500;">Target</div>
<div style="background:#161630;border:1px solid #2d2d50;border-radius:6px;padding:8px 12px;font-size:12px;display:flex;justify-content:space-between;align-items:center;">
<span>OUTPUT</span><span style="color:#9ca3af;font-size:10px;"></span>
</div>
</div>
</div>
<div style="margin-bottom:14px;">
<div style="font-size:11px;color:#9ca3af;margin-bottom:4px;font-weight:500;">Expression <span style="color:#f87171;">*</span></div>
<textarea style="background:#161630;border:1px solid #2d2d50;border-radius:6px;color:#e0e0e0;padding:8px 12px;width:100%;font-family:monospace;font-size:12px;box-sizing:border-box;resize:vertical;min-height:40px;">${body.orderId}</textarea>
</div>
<div style="margin-bottom:14px;">
<div style="font-size:11px;color:#9ca3af;margin-bottom:4px;font-weight:500;">Attribute Type</div>
<div style="display:flex;gap:8px;">
<div style="background:#1e3a5f;color:#7dd3fc;padding:4px 12px;border-radius:6px;font-size:11px;cursor:pointer;border:1px solid #3b82f6;">BUSINESS_OBJECT</div>
<div style="background:#161630;color:#9ca3af;padding:4px 12px;border-radius:6px;font-size:11px;cursor:pointer;border:1px solid #2d2d50;">CORRELATION</div>
<div style="background:#161630;color:#9ca3af;padding:4px 12px;border-radius:6px;font-size:11px;cursor:pointer;border:1px solid #2d2d50;">EVENT</div>
<div style="background:#161630;color:#9ca3af;padding:4px 12px;border-radius:6px;font-size:11px;cursor:pointer;border:1px solid #2d2d50;">CUSTOM</div>
</div>
</div>
<!-- ═══ TEST EXPRESSION SECTION ═══ -->
<div style="border-top:1px solid #2d2d50;margin-top:8px;padding-top:14px;">
<div style="display:flex;align-items:center;gap:6px;margin-bottom:12px;cursor:pointer;">
<span style="color:#60a5fa;font-size:10px;">&#9660;</span>
<span style="font-size:12px;font-weight:600;color:#60a5fa;">Test Expression</span>
</div>
<!-- Data source tabs -->
<div style="display:flex;gap:0;margin-bottom:0;border-bottom:1px solid #2d2d50;">
<div style="padding:6px 14px;font-size:11px;font-weight:600;color:#60a5fa;border-bottom:2px solid #3b82f6;cursor:pointer;">Recent Exchange</div>
<div style="padding:6px 14px;font-size:11px;color:#9ca3af;cursor:pointer;">Custom Payload</div>
</div>
<!-- Recent exchange picker -->
<div style="background:#161630;border:1px solid #2d2d50;border-top:none;border-radius:0 0 6px 6px;padding:12px;">
<div style="margin-bottom:10px;">
<div style="background:#1a1a2e;border:1px solid #2d2d50;border-radius:6px;padding:8px 12px;font-size:11px;display:flex;justify-content:space-between;align-items:center;">
<div style="display:flex;align-items:center;gap:8px;">
<span style="width:7px;height:7px;border-radius:50%;background:#4ade80;display:inline-block;"></span>
<span style="font-family:monospace;color:#e0e0e0;">a1b2c3d4-e5f6-7890</span>
<span style="color:#6b7280;">·</span>
<span style="color:#6b7280;">245ms</span>
<span style="color:#6b7280;">·</span>
<span style="color:#6b7280;">2 min ago</span>
</div>
<span style="color:#9ca3af;font-size:10px;"></span>
</div>
</div>
<!-- Test button + result -->
<div style="display:flex;gap:8px;align-items:flex-start;">
<button style="background:#3b82f6;color:white;border:none;padding:6px 14px;border-radius:6px;font-size:11px;font-weight:600;cursor:pointer;white-space:nowrap;">&#9654; Test</button>
<div style="flex:1;background:#0f2a1a;border:1px solid #166534;border-radius:6px;padding:8px 12px;">
<div style="font-size:10px;color:#6b7280;margin-bottom:2px;">Result</div>
<div style="font-family:monospace;font-size:12px;color:#4ade80;">ORD-2024-78542</div>
</div>
</div>
</div>
</div>
</div>
<!-- Footer -->
<div style="display:flex;gap:8px;justify-content:flex-end;padding:12px 20px;border-top:1px solid #2d2d50;background:#1a1a30;">
<button style="background:transparent;color:#9ca3af;border:1px solid #2d2d50;padding:6px 16px;border-radius:6px;font-size:12px;cursor:pointer;">Cancel</button>
<button style="background:#3b82f6;color:white;border:none;padding:6px 16px;border-radius:6px;font-size:12px;font-weight:600;cursor:pointer;">Save Tap</button>
</div>
</div>
</div>
</div>
<div style="margin-top:24px;"></div>
<div class="mockup">
<div class="mockup-header">Test Expression — Custom Payload Mode</div>
<div class="mockup-body" style="background:#1a1a2e;color:#e0e0e0;padding:40px;display:flex;justify-content:center;">
<div style="background:#1e1e3a;border:1px solid #2d2d50;border-radius:12px;padding:0;width:560px;box-shadow:0 20px 60px rgba(0,0,0,0.5);overflow:hidden;">
<!-- Header -->
<div style="display:flex;justify-content:space-between;align-items:center;padding:16px 20px;border-bottom:1px solid #2d2d50;">
<span style="font-size:15px;font-weight:600;">Add Tap</span>
<span style="color:#9ca3af;cursor:pointer;font-size:18px;"></span>
</div>
<div style="padding:20px;">
<!-- Form fields abbreviated -->
<div style="text-align:center;padding:8px;font-size:11px;color:#6b7280;border:1px dashed #2d2d50;border-radius:6px;margin-bottom:14px;">
⬆ Form fields above (attribute name, processor, language, target, expression, type)
</div>
<!-- ═══ TEST EXPRESSION SECTION ═══ -->
<div style="border-top:1px solid #2d2d50;padding-top:14px;">
<div style="display:flex;align-items:center;gap:6px;margin-bottom:12px;cursor:pointer;">
<span style="color:#60a5fa;font-size:10px;">&#9660;</span>
<span style="font-size:12px;font-weight:600;color:#60a5fa;">Test Expression</span>
</div>
<!-- Data source tabs -->
<div style="display:flex;gap:0;margin-bottom:0;border-bottom:1px solid #2d2d50;">
<div style="padding:6px 14px;font-size:11px;color:#9ca3af;cursor:pointer;">Recent Exchange</div>
<div style="padding:6px 14px;font-size:11px;font-weight:600;color:#60a5fa;border-bottom:2px solid #3b82f6;cursor:pointer;">Custom Payload</div>
</div>
<!-- Custom payload editor -->
<div style="background:#161630;border:1px solid #2d2d50;border-top:none;border-radius:0 0 6px 6px;padding:12px;">
<div style="margin-bottom:10px;">
<textarea style="background:#1a1a2e;border:1px solid #2d2d50;border-radius:6px;color:#e0e0e0;padding:8px 12px;width:100%;font-family:monospace;font-size:11px;box-sizing:border-box;resize:vertical;min-height:100px;line-height:1.5;">{
"orderId": "ORD-2024-78542",
"customer": {
"id": "CUST-1234",
"name": "Acme Corp"
},
"total": 149.90
}</textarea>
</div>
<!-- Test button + error result -->
<div style="display:flex;gap:8px;align-items:flex-start;">
<button style="background:#3b82f6;color:white;border:none;padding:6px 14px;border-radius:6px;font-size:11px;font-weight:600;cursor:pointer;white-space:nowrap;">&#9654; Test</button>
<div style="flex:1;background:#2a0f0f;border:1px solid #991b1b;border-radius:6px;padding:8px 12px;">
<div style="font-size:10px;color:#6b7280;margin-bottom:2px;">Error</div>
<div style="font-family:monospace;font-size:11px;color:#f87171;">Expression evaluation timed out (50ms limit)</div>
</div>
</div>
<div style="font-size:10px;color:#6b7280;margin-top:8px;">Evaluated by agent <span style="font-family:monospace;">order-svc-01</span> using Camel's <span style="font-family:monospace;">simple</span> language</div>
</div>
</div>
</div>
<!-- Footer -->
<div style="display:flex;gap:8px;justify-content:flex-end;padding:12px 20px;border-top:1px solid #2d2d50;background:#1a1a30;">
<button style="background:transparent;color:#9ca3af;border:1px solid #2d2d50;padding:6px 16px;border-radius:6px;font-size:12px;cursor:pointer;">Cancel</button>
<button style="background:#3b82f6;color:white;border:none;padding:6px 16px;border-radius:6px;font-size:12px;font-weight:600;cursor:pointer;">Save Tap</button>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,61 @@
<h2>Traces & Taps — With Route Column</h2>
<p class="subtitle">Route column added to prevent ambiguity across routes</p>
<div class="mockup">
<div class="mockup-header">Traces & Taps — Updated</div>
<div class="mockup-body" style="background:#1a1a2e;color:#e0e0e0;padding:20px;font-family:system-ui,-apple-system,sans-serif;font-size:13px;">
<div style="background:#1e1e3a;border:1px solid #2d2d50;border-radius:8px;padding:16px;">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:10px;">
<div style="font-size:12px;font-weight:600;color:#9ca3af;text-transform:uppercase;letter-spacing:0.5px;">Traces & Taps</div>
<span style="font-size:11px;color:#6b7280;">3 traced · 4 taps · manage taps on route pages</span>
</div>
<table style="width:100%;border-collapse:collapse;font-size:12px;">
<thead>
<tr style="border-bottom:1px solid #2d2d50;">
<th style="text-align:left;padding:8px;color:#9ca3af;font-size:11px;font-weight:500;">Route</th>
<th style="text-align:left;padding:8px;color:#9ca3af;font-size:11px;font-weight:500;">Processor</th>
<th style="text-align:left;padding:8px;color:#9ca3af;font-size:11px;font-weight:500;">Capture</th>
<th style="text-align:left;padding:8px;color:#9ca3af;font-size:11px;font-weight:500;">Taps</th>
</tr>
</thead>
<tbody>
<tr style="border-bottom:1px solid #161630;">
<td style="padding:8px;color:#60a5fa;font-size:11px;">processOrder</td>
<td style="padding:8px;font-family:monospace;font-size:11px;">unmarshal1</td>
<td style="padding:8px;"><span style="background:#2d1f3b;color:#d8b4fe;padding:1px 8px;border-radius:4px;font-size:10px;">BOTH</span></td>
<td style="padding:8px;">
<div style="display:flex;gap:6px;flex-wrap:wrap;">
<span style="background:#1e3a5f;color:#7dd3fc;padding:1px 8px;border-radius:10px;font-size:10px;font-family:monospace;">orderId <span style="color:#4ade80;margin-left:2px;">&#10003;</span></span>
<span style="background:#1e3a5f;color:#7dd3fc;padding:1px 8px;border-radius:10px;font-size:10px;font-family:monospace;">customerId <span style="color:#4ade80;margin-left:2px;">&#10003;</span></span>
</div>
</td>
</tr>
<tr style="border-bottom:1px solid #161630;">
<td style="padding:8px;color:#60a5fa;font-size:11px;">processOrder</td>
<td style="padding:8px;font-family:monospace;font-size:11px;">enrichPrice</td>
<td style="padding:8px;"><span style="color:#6b7280;font-size:11px;"></span></td>
<td style="padding:8px;">
<span style="background:#3b2f1f;color:#fcd34d;padding:1px 8px;border-radius:10px;font-size:10px;font-family:monospace;">orderTotal <span style="color:#6b7280;margin-left:2px;">&#10007;</span></span>
</td>
</tr>
<tr style="border-bottom:1px solid #161630;">
<td style="padding:8px;color:#60a5fa;font-size:11px;">processPayment</td>
<td style="padding:8px;font-family:monospace;font-size:11px;">toDatabase</td>
<td style="padding:8px;"><span style="background:#1e3a5f;color:#7dd3fc;padding:1px 8px;border-radius:4px;font-size:10px;">INPUT</span></td>
<td style="padding:8px;"><span style="color:#6b7280;font-size:11px;"></span></td>
</tr>
<tr>
<td style="padding:8px;color:#60a5fa;font-size:11px;">processPayment</td>
<td style="padding:8px;font-family:monospace;font-size:11px;">validate1</td>
<td style="padding:8px;"><span style="color:#6b7280;font-size:11px;"></span></td>
<td style="padding:8px;">
<span style="background:#1e3a5f;color:#7dd3fc;padding:1px 8px;border-radius:10px;font-size:10px;font-family:monospace;">paymentRef <span style="color:#4ade80;margin-left:2px;">&#10003;</span></span>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>

View File

@@ -0,0 +1,3 @@
<div style="display:flex;align-items:center;justify-content:center;min-height:60vh">
<p class="subtitle">Continuing in terminal...</p>
</div>

View File

@@ -0,0 +1 @@
{"reason":"idle timeout","timestamp":1774552065018}

View File

@@ -0,0 +1 @@
2048

View File

@@ -36,7 +36,7 @@ java -jar cameleer3-server-app/target/cameleer3-server-app-1.0-SNAPSHOT.jar
- Spring Boot 3.4.3 parent POM
- Depends on `com.cameleer3:cameleer3-common` from Gitea Maven registry
- Jackson `JavaTimeModule` for `Instant` deserialization
- Communication: receives HTTP POST data from agents (executions, diagrams, metrics, logs), serves SSE event streams for config push/commands
- Communication: receives HTTP POST data from agents (executions, diagrams, metrics, logs), serves SSE event streams for config push/commands (config-update, deep-trace, replay, route-control)
- Maintains agent instance registry with states: LIVE → STALE → DEAD
- Storage: PostgreSQL (TimescaleDB) for structured data, OpenSearch for full-text search and application log storage
- Security: JWT auth with RBAC (AGENT/VIEWER/OPERATOR/ADMIN roles), Ed25519 config signing, bootstrap token for registration
@@ -57,6 +57,10 @@ java -jar cameleer3-server-app/target/cameleer3-server-app-1.0-SNAPSHOT.jar
- K8s probes: server uses `/api/v1/health`, PostgreSQL uses `pg_isready`, OpenSearch uses `/_cluster/health`
- Docker build uses buildx registry cache + `--provenance=false` for Gitea compatibility
## UI Styling
- Always use `@cameleer/design-system` CSS variables for colors (`var(--amber)`, `var(--error)`, `var(--success)`, etc.) — never hardcode hex values. This applies to CSS modules, inline styles, and SVG `fill`/`stroke` attributes. SVG presentation attributes resolve `var()` correctly.
## Disabled Skills
- Do NOT use any `gsd:*` skills in this project. This includes all `/gsd:` prefixed commands.

View File

@@ -325,6 +325,12 @@ curl -s -X POST http://localhost:8081/api/v1/agents/groups/order-service-prod/co
-H "Authorization: Bearer $TOKEN" \
-d '{"type":"deep-trace","payload":{"routeId":"route-1","durationSeconds":60}}'
# Send route control command to agent group (start/stop/suspend/resume)
curl -s -X POST http://localhost:8081/api/v1/agents/groups/order-service-prod/commands \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $TOKEN" \
-d '{"type":"route-control","payload":{"routeId":"route-1","action":"stop","nonce":"unique-uuid"}}'
# Broadcast command to all live agents
curl -s -X POST http://localhost:8081/api/v1/agents/commands \
-H "Content-Type: application/json" \
@@ -338,7 +344,7 @@ curl -s -X POST http://localhost:8081/api/v1/agents/agent-1/commands/{commandId}
**Agent lifecycle:** LIVE (heartbeat within 90s) → STALE (missed 3 heartbeats) → DEAD (5min after STALE). DEAD agents kept indefinitely.
**SSE events:** `config-update`, `deep-trace`, `replay` commands pushed in real time. Server sends ping keepalive every 15s.
**SSE events:** `config-update`, `deep-trace`, `replay`, `route-control` commands pushed in real time. Server sends ping keepalive every 15s.
**Command expiry:** Unacknowledged commands expire after 60 seconds.

294
UI_FINDINGS.md Normal file
View File

@@ -0,0 +1,294 @@
# UI/UX Evaluation Report — Cameleer3 Server
**Date:** 2026-03-25
**Evaluated URL:** http://192.168.50.86:30090/
**Methodology:** Playwright-driven navigation of all major pages (14 screenshots), evaluated by 3 specialist agents: Visual Design, Information Architecture & Usability, Readability & Accessibility.
---
## Executive Summary
The Cameleer3 dashboard has a **distinctive, well-crafted warm amber design language** that stands out in the observability space. The core monitoring pages (Dashboard, Exchange Detail, Routes, Agents) are polished and consistent. The design system provides a solid foundation.
**Key strengths:** KPI strip pattern, command palette (Ctrl+K), agent card grouping, dark mode token system, cohesive brand identity.
**Critical gaps to address:**
1. **Font sizes too small** — pervasive 10-11px text for critical data impairs reading under stress
2. **Color contrast failures**`--text-muted` and `--text-faint` fail WCAG AA in both themes
3. **Status indicators rely on color alone** — not accessible for color-blind users
4. **Admin infrastructure pages lag in polish** — Database/OpenSearch use ad-hoc styling
5. **Dashboard is a monitoring display, not yet an incident response tool** — missing error highlighting, per-route error breakdowns, actionable status pages
**Overall Score: 7/10** — Strong foundation, needs targeted fixes for production readiness under stress.
---
## Pages Evaluated
| # | Page | Screenshot |
|---|------|-----------|
| 1 | Login | `screenshots/14-login-page.png` |
| 2 | Dashboard (light) | `screenshots/01-dashboard.png` |
| 3 | Dashboard + Detail Panel | `screenshots/02-dashboard-detail-panel.png` |
| 4 | Exchange Detail | `screenshots/03-exchange-detail.png` |
| 5 | Routes Metrics | `screenshots/04-routes-metrics.png` |
| 6 | Agent Health | `screenshots/05-agents.png` |
| 7 | Agent Instance | `screenshots/06-agent-instance.png` |
| 8 | Admin RBAC | `screenshots/07-admin-rbac.png` |
| 9 | Admin Audit Log | `screenshots/08-admin-audit.png` |
| 10 | Admin OIDC | `screenshots/09-admin-oidc.png` |
| 11 | Admin Database | `screenshots/10-admin-database.png` |
| 12 | Admin OpenSearch | `screenshots/11-admin-opensearch.png` |
| 13 | Command Palette | `screenshots/12-command-palette.png` |
| 14 | Dashboard (dark) | `screenshots/13-dashboard-dark-mode.png` |
---
## Page-by-Page Findings
### Login Page
- **[Important]** No brand identity — missing camel logo/icon from sidebar. First impression feels generic.
- **[Important]** Sign-in button color mismatch — uses washed-out gold, not the saturated `--amber` (#C6820E) used throughout the app.
- **[Important]** No SSO/OIDC button visible — system supports OIDC but login page only shows username/password.
- **[Important]** Subtitle text `--text-muted` (#9C9184) on white fails WCAG AA (~2.8:1, needs 4.5:1).
- **[Important]** White text on amber button fails WCAG AA for normal text (~3.2:1).
- **[Nice-to-have]** Card has no shadow/border against the `--bg-body` cream background — minimal separation.
### Dashboard
- **[Important]** Errors KPI card uses red/orange accent border even when errors = 0. Zero-error state should feel reassuring (green/neutral), not alarming. Creates false alarm fatigue.
- **[Important]** Table lacks visible sort indicators — no arrows showing current sort direction.
- **[Important]** Duration column uses color alone (`.durFast` green, `.durSlow` amber, `.durBreach` red) — not color-blind safe.
- **[Important]** Status dots are ~6px — too small to reliably identify, especially for color-blind users.
- **[Critical]** Table meta text at 11px with `--text-muted` is borderline illegible for fatigued users.
- **[Critical]** KPI stat labels at 10px with `--text-muted` — below recommended 12px minimum.
- **[Nice-to-have]** Exchange ID column too wide — truncate to 8 chars with copy-on-click.
### Dashboard — Detail Panel
- **[Important]** Panel lacks clear visual separation from main table — needs left border accent or different background.
- **[Important]** Processor timeline preview in panel is too small to read — adds visual noise without utility.
- **[Critical]** Overview labels at 10px with `--text-muted` — nearly invisible.
- **[Critical]** Panel section meta at 10px with `--text-faint` (#C4BAB0) on white — contrast ratio ~1.9:1, severely fails WCAG AA.
- **[Nice-to-have]** No quick actions (copy exchange ID, view logs, view route diagram).
### Exchange Detail
- **[Critical]** Processor timeline label column too narrow — processor names are truncated/illegible. This is the page's primary visualization.
- **[Critical]** No error highlighting in processor timeline — failed processors need red bars/icons. During incidents, engineers must instantly see WHICH processor failed.
- **[Important]** No linkage to route diagram — "View in Route Diagram" would overlay execution on the visual route graph.
- **[Important]** Long exchange ID in breadcrumb is visually heavy — truncate with copy button.
- **[Important]** Header stat labels at 10px uppercase with `--text-muted` — same contrast issue.
### Routes Metrics
- **[Important]** KPI number formatting inconsistent — Dashboard shows "11.742 ms" (decimal + space), Routes shows "11742ms" (no decimal, no space).
- **[Important]** No per-route error rate column — error rate in KPI strip but not broken down per route.
- **[Important]** Charts disconnected from table — clicking a route should filter/highlight its chart data.
- **[Nice-to-have]** No visual comparison between routes (bar chart or heatmap for quick identification of slowest).
### Agent Health
- **[Critical]** Stale/Dead agent visual distinction is too subtle — at 3am, the difference between LIVE and DEAD must scream. Dead agents should have prominent red background or strikethrough, not just `--text-muted`.
- **[Critical]** Agent state dots (green live, amber stale, gray dead) use color alone — no shape variation for color-blind users.
- **[Important]** "2/26" active routes KPI is ambiguous — unit and meaning need to be explicit.
- **[Nice-to-have]** Timeline at bottom takes significant space — consider making it collapsible.
### Agent Instance Detail
- **[Important]** Charts lack threshold/alert lines — CPU at 2% is fine, but where is "concerning"? Configurable thresholds (CPU > 80%, Memory > 90%) would make charts actionable.
- **[Important]** Chart axis labels appear too small.
- **[Nice-to-have]** GC Pauses uses area fill while others use line charts — minor inconsistency.
- **[Nice-to-have]** Six charts in 2x3 grid can create cognitive overload — consider collapsible groups.
### Admin — RBAC
- **[Important]** KPI strip for "Users: 1, Groups: 2, Roles: 4" has too much visual weight — these low-value numbers don't need full stat-card treatment.
- **[Important]** "ADMIN" role badge vs "ADMINS" group badge look identical — different badge styles needed (outlined for groups, filled for roles).
- **[Nice-to-have]** Empty detail panel ("Select a user to view details") needs icon/illustration.
### Admin — Audit Log
- **[Important]** "no data" empty state is uninformative — should explain "No audit events match your filters" with guidance.
- **[Important]** No export functionality — audit logs need CSV/JSON export for compliance.
- **[Important]** Date range filters use raw datetime inputs — inconsistent with dashboard's polished time range pills.
### Admin — OIDC Config
- **[Critical]** "Delete OIDC Configuration" is a destructive action without confirmation dialog — could lock out all SSO users.
- **[Important]** No inline validation — Issuer URL should validate format on blur, required fields need indicators.
- **[Nice-to-have]** No connection test result display area.
### Admin — Database
- **[Important]** Visual treatment inconsistent with rest of app — "Connected" status and pool stats use ad-hoc text, not design system components.
- **[Important]** Page title "Database Administration" implies actions, but page is read-only — rename to "Database Status" or add operations.
- **[Nice-to-have]** Table row counts should be right-aligned for numerical scanning.
### Admin — OpenSearch
- **[Critical]** "Disconnected" status displayed as plain text — needs error styling (red text, error badge, or status banner). Infrastructure disconnection is a critical state.
- **[Important]** "Yellow" cluster health displayed as plain text with no visual hierarchy — same size/weight as version number and node count.
- **[Important]** Indexing pipeline stats use ad-hoc inline format — should use consistent stat-card pattern.
- **[Important]** "Disconnected" + "Yellow" health shown simultaneously is contradictory — if disconnected, clarify whether data is stale.
### Command Palette
- **[Nice-to-have]** No visible keyboard navigation hint for currently selected item.
- **[Nice-to-have]** Empty palette should show recent/frequent items instead of requiring typing.
- Overall well-executed — categories, counts, keyboard hints in footer.
### Dark Mode
- **[Critical]** `--text-muted` (#7A7068) on `--bg-surface` (#242019) is ~2.9:1 — fails WCAG AA. Affects ALL muted labels across every page.
- **[Critical]** `--text-faint` (#4A4238) on `--bg-surface` (#242019) is ~1.4:1 — catastrophically fails WCAG AA. Essentially invisible.
- **[Important]** `--amber` (#D4941E) on `--bg-surface` (#242019) is ~3.6:1 — amber links/active text fail AA.
- **[Important]** KPI sparkline chart lines are harder to read — thin strokes need increased width or brightness.
- **[Important]** Sidebar boundary contrast drops significantly (`--sidebar-bg` #141210 vs `--bg-body` #1A1714 is only ~6 units apart).
- **[Important]** Table row alternation contrast near zero in dark mode.
- **[Nice-to-have]** Amber accent color shift from #C6820E to #D4941E is well-handled.
- **[Nice-to-have]** Semantic colors (success, error, warning) appropriately increase luminance.
---
## Cross-Cutting Issues
### 1. Color Contrast (WCAG AA Failures)
**Light Mode:**
| Element | Foreground | Background | Ratio | Required | Verdict |
|---------|-----------|------------|-------|----------|---------|
| StatCard labels, table meta, section headers | `--text-muted` #9C9184 | #FFFFFF | ~3.0:1 | 4.5:1 | **FAIL** |
| Panel meta, overview hints | `--text-faint` #C4BAB0 | #FFFFFF | ~1.9:1 | 4.5:1 | **FAIL** |
| Sign-in button text | #FFFFFF | `--amber` #C6820E | ~3.2:1 | 4.5:1 | **FAIL** |
| Sidebar muted text | #9C9184 | `--sidebar-bg` #2C2520 | ~3.1:1 | 4.5:1 | **FAIL** |
**Dark Mode:**
| Element | Foreground | Background | Ratio | Required | Verdict |
|---------|-----------|------------|-------|----------|---------|
| All muted labels | #7A7068 | #242019 | ~2.9:1 | 4.5:1 | **FAIL** |
| All faint hints | #4A4238 | #242019 | ~1.4:1 | 4.5:1 | **FAIL** |
| Amber links/active text | #D4941E | #242019 | ~3.6:1 | 4.5:1 | **FAIL** |
**Fix:** Change `--text-muted` to **#766A5E** (light) / **#9A9088** (dark). Restrict `--text-faint` to decorative use only or lighten dark variant to #6A6058.
### 2. Font Size Floor
10px text is used for: StatCard labels, overview labels, chain labels, section meta, error class names, detail labels, sidebar tree labels. 11px is used for: table meta, error messages, pagination, toggle buttons, chart titles.
**Fix:** Establish `--font-size-min: 12px` as a design system floor. Update all 10px instances to 12px, all 11px instances to 12px.
### 3. Number/Unit Formatting
Inconsistent across pages:
- Dashboard: "11.742 ms" (decimal + space)
- Routes: "11742ms" (no decimal, no space)
- Dashboard: "1.1 msg/s" vs Agent Instance: "0.1/s"
**Fix:** Create a shared formatting utility enforcing: consistent decimal precision, space before unit, consistent abbreviations.
### 4. KPI Strip Inconsistency
Used on Dashboard, Routes, Agents, Agent Instance (consistent). But RBAC uses oversized cards for trivial counts, and Database/OpenSearch use ad-hoc text rendering.
**Fix:** Admin infra pages should adopt KPI stat strip or a compact-stat component.
### 5. Empty States
Inconsistent handling:
- Audit Log: "no data" in plain gray
- RBAC detail: "Select a user to view details" in gray
- No consistent empty state component with icon + message + CTA
**Fix:** Design system EmptyState component with icon, message, and optional action.
### 6. Status Indicator Accessibility
Color-only status encoding throughout:
- Duration: green (fast), amber (slow), red (breach) — no icons
- Status dots: green (live), amber (stale), gray (dead) — no shapes
- Agent dead state uses `--text-muted` instead of `--error`
**Fix:** Add shape variation (checkmark/triangle/X), increase dot size to 10px minimum, always render text label alongside.
### 7. Sidebar Structure
Same apps listed 3x (under Applications, Agents, Routes) — triples sidebar length and scales poorly.
**Fix:** Unified application-centric tree where expanding an app shows its agents and routes as children.
---
## Prioritized Recommendations
### Critical (fix now)
| # | Recommendation | Impact |
|---|---------------|--------|
| 1 | **Bump `--text-muted` to WCAG AA compliance**#766A5E (light) / #9A9088 (dark). Single highest-impact fix across all pages. | Fixes majority of contrast failures |
| 2 | **Establish 12px minimum font size** — update all 10px and 11px instances. Especially StatCard labels, overview labels, table meta. | Readable under stress |
| 3 | **Add error highlighting to processor timeline** — red bars, error icons for failed processors. Core debugging view. | Incident response speed |
| 4 | **Make Stale/Dead agent states unmistakable** — full card background color (yellow stale, red dead), prominent badge. Change dead from `--text-muted` to `--error`. | Prevents missed outages |
| 5 | **Fix OpenSearch "Disconnected" status** — use error badge/banner, add "Reconnect" action, clarify stale data. | Actionable admin page |
| 6 | **Add confirmation dialog for OIDC deletion** — type-to-confirm to prevent locking out SSO users. | Prevents lockout |
| 7 | **Color Errors KPI card conditionally** — green/neutral at 0, red only when > 0. Prevents false alarm fatigue. | Reduces noise |
### Important (next sprint)
| # | Recommendation | Impact |
|---|---------------|--------|
| 8 | **Add secondary encoding to status indicators** — shapes (checkmark/triangle/X) alongside color dots. Increase dot size to 10px+. | Accessibility compliance |
| 9 | **Standardize number/unit formatting** — shared utility for decimals, spacing, unit abbreviations. | Visual consistency |
| 10 | **Add per-route error rate to Routes table** — essential for isolating failing routes. | Incident triage |
| 11 | **Add visible sort indicators to data tables** — arrows on column headers. | Data exploration |
| 12 | **Bring admin infra pages to design system quality** — replace ad-hoc text with KPI strips/stat cards. | Professional polish |
| 13 | **Fix login page brand identity** — add camel logo, use correct `--amber` for button, add SSO button when OIDC configured. | First impression |
| 14 | **Fix dark mode specifics** — increase sidebar boundary contrast (add 1px border), boost chart stroke width, fix amber link contrast. | Dark mode usability |
| 15 | **Widen processor timeline label column** — prevent name truncation, add tooltips for long names. | Core visualization |
| 16 | **Add detail panel visual separation** — 2px left border accent. | Layout clarity |
| 17 | **Pin Admin/API Docs to sidebar footer** — accessible without scrolling. | Navigation |
| 18 | **Audit log improvements** — informative empty state, CSV/JSON export, date picker consistent with dashboard. | Admin usability |
| 19 | **OIDC form validation** — inline URL validation, required field indicators, test result display. | Configuration safety |
| 20 | **Fix amber button text contrast** — darken button to #8B5A06 or use dark text on amber. | Accessibility |
### Nice-to-have (backlog)
| # | Recommendation | Impact |
|---|---------------|--------|
| 21 | Unify sidebar into single application-centric tree (Applications > agents + routes) | Scalability |
| 22 | Truncate Exchange IDs to 8 chars with copy-on-click | Table space |
| 23 | Add threshold/alert lines to agent metric charts | Actionable monitoring |
| 24 | Link charts to table selection on Routes Metrics | Data exploration |
| 25 | Add clickable KPI cards navigating to filtered views | Navigation shortcuts |
| 26 | Add `prefers-reduced-motion` support for StatusDot pulse animation | Accessibility |
| 27 | Add tooltips to sparkline charts showing value on hover | Data context |
| 28 | Replace hardcoded `#5db866` in Dashboard.module.css with `var(--success)` | Token compliance |
| 29 | Add keyboard navigation indicators to command palette (selected item highlight) | Power user UX |
| 30 | Show recent/frequent items in empty command palette | Discoverability |
| 31 | Consolidate duplicated table-header CSS into design system component | Maintainability |
| 32 | Login page card shadow for visual lift | Polish |
| 33 | Collapsible agent event timeline | Space efficiency |
| 34 | Dark mode `--text-faint` increase to #6A6058 for 3:1 minimum | Accessibility |
| 35 | Increase DataTable row height to 44px (touch target minimum) | Accessibility |
---
## Dark Mode Assessment
**Grade: Good foundation, specific contrast concerns.**
**What works well:**
- Token system remaps all semantic colors without introducing cold blue-grays — warm brand preserved
- Amber accent brightens appropriately (#C6820E#D4941E)
- Error/warning/success colors increase luminance correctly
- Shadows shift from warm semi-transparent to opaque — correct for dark backgrounds
**What needs fixing:**
- Sidebar contrast: `--sidebar-bg` #141210 vs `--bg-body` #1A1714 only ~6 units apart (was ~50 in light mode)
- Chart line visibility: thin 1-2px strokes need increased width
- Table row alternation: near-zero contrast between `--bg-surface` and `--bg-raised`
- `--text-faint`: essentially invisible at 1.4:1 contrast
- `--text-muted`: 2.9:1 — below AA minimum

View File

@@ -5,6 +5,8 @@ import com.cameleer3.server.app.dto.CommandAckRequest;
import com.cameleer3.server.app.dto.CommandBroadcastResponse;
import com.cameleer3.server.app.dto.CommandRequest;
import com.cameleer3.server.app.dto.CommandSingleResponse;
import com.cameleer3.server.app.dto.ReplayRequest;
import com.cameleer3.server.app.dto.ReplayResponse;
import com.cameleer3.server.core.admin.AuditCategory;
import com.cameleer3.server.core.admin.AuditResult;
import com.cameleer3.server.core.admin.AuditService;
@@ -13,6 +15,7 @@ import com.cameleer3.server.core.agent.AgentEventService;
import com.cameleer3.server.core.agent.AgentInfo;
import com.cameleer3.server.core.agent.AgentRegistryService;
import com.cameleer3.server.core.agent.AgentState;
import com.cameleer3.server.core.agent.CommandReply;
import com.cameleer3.server.core.agent.CommandType;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
@@ -32,7 +35,14 @@ import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.server.ResponseStatusException;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
/**
* Command push endpoints for sending commands to agents via SSE.
@@ -184,6 +194,75 @@ public class AgentCommandController {
return ResponseEntity.ok().build();
}
@PostMapping("/{id}/replay")
@Operation(summary = "Replay an exchange on a specific agent (synchronous)",
description = "Sends a replay command and waits for the agent to complete the replay. "
+ "Returns the replay result including status, replayExchangeId, and duration.")
@ApiResponse(responseCode = "200", description = "Replay completed (check status for success/failure)")
@ApiResponse(responseCode = "404", description = "Agent not found or not connected")
@ApiResponse(responseCode = "504", description = "Agent did not respond in time")
public ResponseEntity<ReplayResponse> replayExchange(@PathVariable String id,
@RequestBody ReplayRequest request,
HttpServletRequest httpRequest) {
AgentInfo agent = registryService.findById(id);
if (agent == null) {
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Agent not found: " + id);
}
// Build protocol-compliant replay payload
Map<String, Object> payload = new LinkedHashMap<>();
payload.put("routeId", request.routeId());
Map<String, Object> exchange = new LinkedHashMap<>();
exchange.put("body", request.body() != null ? request.body() : "");
exchange.put("headers", request.headers() != null ? request.headers() : Map.of());
payload.put("exchange", exchange);
if (request.originalExchangeId() != null) {
payload.put("originalExchangeId", request.originalExchangeId());
}
payload.put("nonce", UUID.randomUUID().toString());
String payloadJson;
try {
payloadJson = objectMapper.writeValueAsString(payload);
} catch (JsonProcessingException e) {
log.error("Failed to serialize replay payload", e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(new ReplayResponse("FAILURE", "Failed to serialize request", null));
}
CompletableFuture<CommandReply> future = registryService.addCommandWithReply(
id, CommandType.REPLAY, payloadJson);
Map<String, Object> auditDetails = new LinkedHashMap<>();
auditDetails.put("routeId", request.routeId());
if (request.originalExchangeId() != null) {
auditDetails.put("originalExchangeId", request.originalExchangeId());
}
try {
CommandReply reply = future.orTimeout(30, TimeUnit.SECONDS).join();
auditDetails.put("replyStatus", reply.status());
auditDetails.put("replyMessage", reply.message() != null ? reply.message() : "");
auditService.log("replay_exchange", AuditCategory.AGENT, id, auditDetails,
"SUCCESS".equals(reply.status()) ? AuditResult.SUCCESS : AuditResult.FAILURE, httpRequest);
return ResponseEntity.ok(new ReplayResponse(reply.status(), reply.message(), reply.data()));
} catch (CompletionException e) {
if (e.getCause() instanceof TimeoutException) {
auditDetails.put("error", "timeout");
auditService.log("replay_exchange", AuditCategory.AGENT, id, auditDetails,
AuditResult.FAILURE, httpRequest);
return ResponseEntity.status(HttpStatus.GATEWAY_TIMEOUT)
.body(new ReplayResponse("FAILURE", "Agent did not respond within 30 seconds", null));
}
auditDetails.put("error", e.getCause().getMessage());
auditService.log("replay_exchange", AuditCategory.AGENT, id, auditDetails,
AuditResult.FAILURE, httpRequest);
log.error("Error awaiting replay reply from agent {}", id, e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(new ReplayResponse("FAILURE", "Internal error: " + e.getCause().getMessage(), null));
}
}
private CommandType mapCommandType(String typeStr) {
return switch (typeStr) {
case "config-update" -> CommandType.CONFIG_UPDATE;
@@ -191,8 +270,9 @@ public class AgentCommandController {
case "replay" -> CommandType.REPLAY;
case "set-traced-processors" -> CommandType.SET_TRACED_PROCESSORS;
case "test-expression" -> CommandType.TEST_EXPRESSION;
case "route-control" -> CommandType.ROUTE_CONTROL;
default -> throw new ResponseStatusException(HttpStatus.BAD_REQUEST,
"Invalid command type: " + typeStr + ". Valid: config-update, deep-trace, replay, set-traced-processors, test-expression");
"Invalid command type: " + typeStr + ". Valid: config-update, deep-trace, replay, set-traced-processors, test-expression, route-control");
};
}
}

View File

@@ -14,7 +14,8 @@ public class ApiExceptionHandler {
@ExceptionHandler(ResponseStatusException.class)
public ResponseEntity<ErrorResponse> handleResponseStatus(ResponseStatusException ex) {
String reason = ex.getReason();
return ResponseEntity.status(ex.getStatusCode())
.body(new ErrorResponse(ex.getReason() != null ? ex.getReason() : "Unknown error"));
.body(new ErrorResponse(reason != null ? reason : "Unknown error"));
}
}

View File

@@ -0,0 +1,79 @@
package com.cameleer3.server.app.controller;
import com.cameleer3.server.app.dto.AppSettingsRequest;
import com.cameleer3.server.core.admin.AppSettings;
import com.cameleer3.server.core.admin.AppSettingsRepository;
import com.cameleer3.server.core.admin.AuditCategory;
import com.cameleer3.server.core.admin.AuditResult;
import com.cameleer3.server.core.admin.AuditService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.validation.Valid;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.server.ResponseStatusException;
import java.util.List;
import java.util.Map;
@RestController
@RequestMapping("/api/v1/admin/app-settings")
@PreAuthorize("hasAnyRole('ADMIN', 'OPERATOR')")
@Tag(name = "App Settings", description = "Per-application dashboard settings (ADMIN/OPERATOR)")
public class AppSettingsController {
private final AppSettingsRepository repository;
private final AuditService auditService;
public AppSettingsController(AppSettingsRepository repository, AuditService auditService) {
this.repository = repository;
this.auditService = auditService;
}
@GetMapping
@Operation(summary = "List all application settings")
public ResponseEntity<List<AppSettings>> getAll() {
return ResponseEntity.ok(repository.findAll());
}
@GetMapping("/{appId}")
@Operation(summary = "Get settings for a specific application (returns defaults if not configured)")
public ResponseEntity<AppSettings> getByAppId(@PathVariable String appId) {
AppSettings settings = repository.findByAppId(appId).orElse(AppSettings.defaults(appId));
return ResponseEntity.ok(settings);
}
@PutMapping("/{appId}")
@Operation(summary = "Create or update settings for an application")
public ResponseEntity<AppSettings> update(@PathVariable String appId,
@Valid @RequestBody AppSettingsRequest request,
HttpServletRequest httpRequest) {
List<String> errors = request.validate();
if (!errors.isEmpty()) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, String.join("; ", errors));
}
AppSettings saved = repository.save(request.toSettings(appId));
auditService.log("update_app_settings", AuditCategory.CONFIG, appId,
Map.of("settings", saved), AuditResult.SUCCESS, httpRequest);
return ResponseEntity.ok(saved);
}
@DeleteMapping("/{appId}")
@Operation(summary = "Delete application settings (reverts to defaults)")
public ResponseEntity<Void> delete(@PathVariable String appId, HttpServletRequest httpRequest) {
repository.delete(appId);
auditService.log("delete_app_settings", AuditCategory.CONFIG, appId,
Map.of(), AuditResult.SUCCESS, httpRequest);
return ResponseEntity.noContent().build();
}
}

View File

@@ -59,7 +59,8 @@ public class DatabaseAdminController {
String host = extractHost(dataSource);
return ResponseEntity.ok(new DatabaseStatusResponse(true, version, host, schema, timescaleDb));
} catch (Exception e) {
return ResponseEntity.ok(new DatabaseStatusResponse(false, null, null, null, false));
return ResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE)
.body(new DatabaseStatusResponse(false, null, null, null, false));
}
}

View File

@@ -49,7 +49,7 @@ public class DetailController {
}
@GetMapping("/{executionId}/processors/{index}/snapshot")
@Operation(summary = "Get exchange snapshot for a specific processor")
@Operation(summary = "Get exchange snapshot for a specific processor by index")
@ApiResponse(responseCode = "200", description = "Snapshot data")
@ApiResponse(responseCode = "404", description = "Snapshot not found")
public ResponseEntity<Map<String, String>> getProcessorSnapshot(
@@ -69,4 +69,16 @@ public class DetailController {
return ResponseEntity.ok(snapshot);
}
@GetMapping("/{executionId}/processors/by-id/{processorId}/snapshot")
@Operation(summary = "Get exchange snapshot for a specific processor by processorId")
@ApiResponse(responseCode = "200", description = "Snapshot data")
@ApiResponse(responseCode = "404", description = "Snapshot not found")
public ResponseEntity<Map<String, String>> processorSnapshotById(
@PathVariable String executionId,
@PathVariable String processorId) {
return detailService.getProcessorSnapshot(executionId, processorId)
.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
}
}

View File

@@ -80,7 +80,8 @@ public class OpenSearchAdminController {
health.numberOfNodes(),
opensearchUrl));
} catch (Exception e) {
return ResponseEntity.ok(new OpenSearchStatusResponse(
return ResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE)
.body(new OpenSearchStatusResponse(
false, "UNREACHABLE", null, 0, opensearchUrl));
}
}
@@ -149,7 +150,8 @@ public class OpenSearchAdminController {
pageItems, totalIndices, totalDocs,
humanSize(totalBytes), page, size, totalPages));
} catch (Exception e) {
return ResponseEntity.ok(new IndicesPageResponse(
return ResponseEntity.status(HttpStatus.BAD_GATEWAY)
.body(new IndicesPageResponse(
List.of(), 0, 0, "0 B", page, size, 0));
}
}
@@ -234,7 +236,8 @@ public class OpenSearchAdminController {
searchLatency, indexingLatency,
heapUsed, heapMax));
} catch (Exception e) {
return ResponseEntity.ok(new PerformanceResponse(0, 0, 0, 0, 0, 0));
return ResponseEntity.status(HttpStatus.BAD_GATEWAY)
.body(new PerformanceResponse(0, 0, 0, 0, 0, 0));
}
}

View File

@@ -3,9 +3,11 @@ package com.cameleer3.server.app.controller;
import com.cameleer3.server.app.dto.AgentSummary;
import com.cameleer3.server.app.dto.AppCatalogEntry;
import com.cameleer3.server.app.dto.RouteSummary;
import com.cameleer3.common.graph.RouteGraph;
import com.cameleer3.server.core.agent.AgentInfo;
import com.cameleer3.server.core.agent.AgentRegistryService;
import com.cameleer3.server.core.agent.AgentState;
import com.cameleer3.server.core.storage.DiagramStore;
import com.cameleer3.server.core.storage.StatsStore;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
@@ -34,10 +36,14 @@ import java.util.stream.Collectors;
public class RouteCatalogController {
private final AgentRegistryService registryService;
private final DiagramStore diagramStore;
private final JdbcTemplate jdbc;
public RouteCatalogController(AgentRegistryService registryService, JdbcTemplate jdbc) {
public RouteCatalogController(AgentRegistryService registryService,
DiagramStore diagramStore,
JdbcTemplate jdbc) {
this.registryService = registryService;
this.diagramStore = diagramStore;
this.jdbc = jdbc;
}
@@ -114,12 +120,14 @@ public class RouteCatalogController {
// Routes
Set<String> routeIds = routesByApp.getOrDefault(appId, Set.of());
List<String> agentIds = agents.stream().map(AgentInfo::id).toList();
List<RouteSummary> routeSummaries = routeIds.stream()
.map(routeId -> {
String key = appId + "/" + routeId;
long count = routeExchangeCounts.getOrDefault(key, 0L);
Instant lastSeen = routeLastSeen.get(key);
return new RouteSummary(routeId, count, lastSeen);
String fromUri = resolveFromEndpointUri(routeId, agentIds);
return new RouteSummary(routeId, count, lastSeen, fromUri);
})
.toList();
@@ -141,6 +149,15 @@ public class RouteCatalogController {
return ResponseEntity.ok(catalog);
}
/** Resolve the from() endpoint URI for a route by looking up its diagram. */
private String resolveFromEndpointUri(String routeId, List<String> agentIds) {
return diagramStore.findContentHashForRouteByAgents(routeId, agentIds)
.flatMap(diagramStore::findByContentHash)
.map(RouteGraph::getRoot)
.map(root -> root.getEndpointUri())
.orElse(null);
}
private String computeWorstHealth(List<AgentInfo> agents) {
boolean hasDead = false;
boolean hasStale = false;

View File

@@ -2,6 +2,9 @@ package com.cameleer3.server.app.controller;
import com.cameleer3.server.app.dto.ProcessorMetrics;
import com.cameleer3.server.app.dto.RouteMetrics;
import com.cameleer3.server.core.admin.AppSettings;
import com.cameleer3.server.core.admin.AppSettingsRepository;
import com.cameleer3.server.core.storage.StatsStore;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag;
@@ -18,6 +21,7 @@ import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
@RestController
@RequestMapping("/api/v1/routes")
@@ -25,9 +29,14 @@ import java.util.List;
public class RouteMetricsController {
private final JdbcTemplate jdbc;
private final StatsStore statsStore;
private final AppSettingsRepository appSettingsRepository;
public RouteMetricsController(JdbcTemplate jdbc) {
public RouteMetricsController(JdbcTemplate jdbc, StatsStore statsStore,
AppSettingsRepository appSettingsRepository) {
this.jdbc = jdbc;
this.statsStore = statsStore;
this.appSettingsRepository = appSettingsRepository;
}
@GetMapping("/metrics")
@@ -78,7 +87,7 @@ public class RouteMetricsController {
routeKeys.add(new RouteKey(applicationName, routeId));
return new RouteMetrics(routeId, applicationName, total, successRate,
avgDur, p99Dur, errorRate, tps, List.of());
avgDur, p99Dur, errorRate, tps, List.of(), -1.0);
}, params.toArray());
// Fetch sparklines (12 buckets over the time window)
@@ -100,13 +109,34 @@ public class RouteMetricsController {
m.appId(), m.routeId());
metrics.set(i, new RouteMetrics(m.routeId(), m.appId(), m.exchangeCount(),
m.successRate(), m.avgDurationMs(), m.p99DurationMs(),
m.errorRate(), m.throughputPerSec(), sparkline));
m.errorRate(), m.throughputPerSec(), sparkline, m.slaCompliance()));
} catch (Exception e) {
// Leave sparkline empty on error
}
}
}
// Enrich with SLA compliance per route
if (!metrics.isEmpty()) {
// Determine SLA threshold (per-app or default)
String effectiveAppId = appId != null ? appId : (metrics.isEmpty() ? null : metrics.get(0).appId());
int threshold = appSettingsRepository.findByAppId(effectiveAppId != null ? effectiveAppId : "")
.map(AppSettings::slaThresholdMs).orElse(300);
Map<String, long[]> slaCounts = statsStore.slaCountsByRoute(fromInstant, toInstant,
effectiveAppId, threshold);
for (int i = 0; i < metrics.size(); i++) {
RouteMetrics m = metrics.get(i);
long[] counts = slaCounts.get(m.routeId());
double sla = (counts != null && counts[1] > 0)
? counts[0] * 100.0 / counts[1] : 100.0;
metrics.set(i, new RouteMetrics(m.routeId(), m.appId(), m.exchangeCount(),
m.successRate(), m.avgDurationMs(), m.p99DurationMs(),
m.errorRate(), m.throughputPerSec(), m.sparkline(), sla));
}
}
return ResponseEntity.ok(metrics);
}

View File

@@ -1,5 +1,7 @@
package com.cameleer3.server.app.controller;
import com.cameleer3.server.core.admin.AppSettings;
import com.cameleer3.server.core.admin.AppSettingsRepository;
import com.cameleer3.server.core.agent.AgentInfo;
import com.cameleer3.server.core.agent.AgentRegistryService;
import com.cameleer3.server.core.search.ExecutionStats;
@@ -8,6 +10,8 @@ import com.cameleer3.server.core.search.SearchRequest;
import com.cameleer3.server.core.search.SearchResult;
import com.cameleer3.server.core.search.SearchService;
import com.cameleer3.server.core.search.StatsTimeseries;
import com.cameleer3.server.core.search.TopError;
import com.cameleer3.server.core.storage.StatsStore;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.http.ResponseEntity;
@@ -20,6 +24,7 @@ import org.springframework.web.bind.annotation.RestController;
import java.time.Instant;
import java.util.List;
import java.util.Map;
/**
* Search endpoints for querying route executions.
@@ -34,10 +39,13 @@ public class SearchController {
private final SearchService searchService;
private final AgentRegistryService registryService;
private final AppSettingsRepository appSettingsRepository;
public SearchController(SearchService searchService, AgentRegistryService registryService) {
public SearchController(SearchService searchService, AgentRegistryService registryService,
AppSettingsRepository appSettingsRepository) {
this.searchService = searchService;
this.registryService = registryService;
this.appSettingsRepository = appSettingsRepository;
}
@GetMapping("/executions")
@@ -87,21 +95,29 @@ public class SearchController {
}
@GetMapping("/stats")
@Operation(summary = "Aggregate execution stats (P99 latency, active count)")
@Operation(summary = "Aggregate execution stats (P99 latency, active count, SLA compliance)")
public ResponseEntity<ExecutionStats> stats(
@RequestParam Instant from,
@RequestParam(required = false) Instant to,
@RequestParam(required = false) String routeId,
@RequestParam(required = false) String application) {
Instant end = to != null ? to : Instant.now();
ExecutionStats stats;
if (routeId == null && application == null) {
return ResponseEntity.ok(searchService.stats(from, end));
stats = searchService.stats(from, end);
} else if (routeId == null) {
stats = searchService.statsForApp(from, end, application);
} else {
List<String> agentIds = resolveApplicationToAgentIds(application);
stats = searchService.stats(from, end, routeId, agentIds);
}
if (routeId == null) {
return ResponseEntity.ok(searchService.statsForApp(from, end, application));
}
List<String> agentIds = resolveApplicationToAgentIds(application);
return ResponseEntity.ok(searchService.stats(from, end, routeId, agentIds));
// Enrich with SLA compliance
int threshold = appSettingsRepository
.findByAppId(application != null ? application : "")
.map(AppSettings::slaThresholdMs).orElse(300);
double sla = searchService.slaCompliance(from, end, threshold, application, routeId);
return ResponseEntity.ok(stats.withSlaCompliance(sla));
}
@GetMapping("/stats/timeseries")
@@ -126,6 +142,48 @@ public class SearchController {
return ResponseEntity.ok(searchService.timeseries(from, end, buckets, routeId, agentIds));
}
@GetMapping("/stats/timeseries/by-app")
@Operation(summary = "Timeseries grouped by application")
public ResponseEntity<Map<String, StatsTimeseries>> timeseriesByApp(
@RequestParam Instant from,
@RequestParam(required = false) Instant to,
@RequestParam(defaultValue = "24") int buckets) {
Instant end = to != null ? to : Instant.now();
return ResponseEntity.ok(searchService.timeseriesGroupedByApp(from, end, buckets));
}
@GetMapping("/stats/timeseries/by-route")
@Operation(summary = "Timeseries grouped by route for an application")
public ResponseEntity<Map<String, StatsTimeseries>> timeseriesByRoute(
@RequestParam Instant from,
@RequestParam(required = false) Instant to,
@RequestParam(defaultValue = "24") int buckets,
@RequestParam String application) {
Instant end = to != null ? to : Instant.now();
return ResponseEntity.ok(searchService.timeseriesGroupedByRoute(from, end, buckets, application));
}
@GetMapping("/stats/punchcard")
@Operation(summary = "Transaction punchcard: weekday x hour grid (rolling 7 days)")
public ResponseEntity<List<StatsStore.PunchcardCell>> punchcard(
@RequestParam(required = false) String application) {
Instant to = Instant.now();
Instant from = to.minus(java.time.Duration.ofDays(7));
return ResponseEntity.ok(searchService.punchcard(from, to, application));
}
@GetMapping("/errors/top")
@Operation(summary = "Top N errors with velocity trend")
public ResponseEntity<List<TopError>> topErrors(
@RequestParam Instant from,
@RequestParam(required = false) Instant to,
@RequestParam(required = false) String application,
@RequestParam(required = false) String routeId,
@RequestParam(defaultValue = "5") int limit) {
Instant end = to != null ? to : Instant.now();
return ResponseEntity.ok(searchService.topErrors(from, end, application, routeId, limit));
}
/**
* Resolve an application name to agent IDs.
* Returns null if application is null/blank (no filtering).

View File

@@ -9,9 +9,11 @@ import com.cameleer3.server.core.diagram.DiagramRenderer;
import com.cameleer3.server.core.diagram.PositionedEdge;
import com.cameleer3.server.core.diagram.PositionedNode;
import org.eclipse.elk.alg.layered.options.LayeredMetaDataProvider;
import org.eclipse.elk.alg.layered.options.NodePlacementStrategy;
import org.eclipse.elk.core.RecursiveGraphLayoutEngine;
import org.eclipse.elk.core.options.CoreOptions;
import org.eclipse.elk.core.options.Direction;
import org.eclipse.elk.core.options.EdgeRouting;
import org.eclipse.elk.core.options.HierarchyHandling;
import org.eclipse.elk.core.util.BasicProgressMonitor;
import org.eclipse.elk.graph.ElkBendPoint;
@@ -31,6 +33,7 @@ import java.util.ArrayList;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
@@ -38,21 +41,24 @@ import java.util.Set;
/**
* ELK + JFreeSVG implementation of {@link DiagramRenderer}.
* <p>
* Uses Eclipse ELK layered algorithm for top-to-bottom layout computation
* Uses Eclipse ELK layered algorithm for layout computation
* and JFreeSVG for SVG document generation with color-coded nodes.
*/
public class ElkDiagramRenderer implements DiagramRenderer {
// Register ELK provider once (not per-instance)
static {
org.eclipse.elk.core.data.LayoutMetaDataService.getInstance()
.registerLayoutMetaDataProviders(new LayeredMetaDataProvider());
}
private static final int PADDING = 20;
private static final int NODE_HEIGHT = 40;
private static final int NODE_WIDTH = 160;
private static final int MIN_NODE_WIDTH = 80;
private static final int CHAR_WIDTH = 8;
private static final int LABEL_PADDING = 32;
private static final int NODE_HEIGHT = 56;
private static final int NODE_WIDTH = 220;
private static final int COMPOUND_TOP_PADDING = 30;
private static final int COMPOUND_SIDE_PADDING = 10;
private static final int CORNER_RADIUS = 8;
private static final double NODE_SPACING = 90.0;
private static final double NODE_SPACING = 120.0;
private static final double EDGE_SPACING = 20.0;
// Blue: endpoints
@@ -103,16 +109,14 @@ public class ElkDiagramRenderer implements DiagramRenderer {
NodeType.DO_TRY, NodeType.DO_CATCH, NodeType.DO_FINALLY,
NodeType.EIP_LOOP, NodeType.EIP_MULTICAST,
NodeType.EIP_AGGREGATE, NodeType.ON_EXCEPTION, NodeType.ERROR_HANDLER,
NodeType.ON_COMPLETION
NodeType.ON_COMPLETION, NodeType.EIP_CIRCUIT_BREAKER,
NodeType.EIP_FILTER, NodeType.EIP_IDEMPOTENT_CONSUMER, NodeType.EIP_RECIPIENT_LIST
);
public ElkDiagramRenderer() {
// Ensure the layered algorithm meta data provider is registered.
// LayoutMetaDataService uses ServiceLoader, but explicit registration
// guarantees availability regardless of classpath ordering.
org.eclipse.elk.core.data.LayoutMetaDataService.getInstance()
.registerLayoutMetaDataProviders(new LayeredMetaDataProvider());
}
/** Top-level handler types laid out in their own separate ELK graph. */
private static final Set<NodeType> HANDLER_SECTION_TYPES = EnumSet.of(
NodeType.ON_EXCEPTION, NodeType.ERROR_HANDLER, NodeType.ON_COMPLETION
);
@Override
public String renderSvg(RouteGraph graph) {
@@ -136,7 +140,17 @@ public class ElkDiagramRenderer implements DiagramRenderer {
Font labelFont = new Font("SansSerif", Font.PLAIN, 12);
g2.setFont(labelFont);
// Draw compound containers first (background)
// Collect IDs of nodes drawn inside compounds (to avoid double-drawing)
Set<String> compoundChildIds = new HashSet<>();
for (PositionedNode node : allNodes(layout.nodes())) {
if (result.compoundInfos.containsKey(node.id()) && node.children() != null) {
for (PositionedNode child : node.children()) {
collectAllIds(child, compoundChildIds);
}
}
}
// Draw compound containers first (background + children inside)
for (Map.Entry<String, CompoundInfo> entry : result.compoundInfos.entrySet()) {
CompoundInfo ci = entry.getValue();
PositionedNode pn = findNode(layout.nodes(), ci.nodeId);
@@ -145,8 +159,9 @@ public class ElkDiagramRenderer implements DiagramRenderer {
}
}
// Draw leaf nodes
// Draw leaf nodes (skip compounds and their children — already drawn above)
for (PositionedNode node : allNodes(layout.nodes())) {
if (compoundChildIds.contains(node.id())) continue;
if (!result.compoundInfos.containsKey(node.id()) || node.children().isEmpty()) {
drawNode(g2, node, result.nodeColors.getOrDefault(node.id(), PURPLE));
}
@@ -171,109 +186,249 @@ public class ElkDiagramRenderer implements DiagramRenderer {
// ----------------------------------------------------------------
private LayoutResult computeLayout(RouteGraph graph, Direction rootDirection) {
ElkGraphFactory factory = ElkGraphFactory.eINSTANCE;
// 1. Build node index from root tree (preserves children) with flat-list fallback
Map<String, RouteNode> nodeById = buildNodeIndex(graph);
LayoutContext ctx = new LayoutContext(ElkGraphFactory.eINSTANCE, nodeById);
// Create root node
ElkNode rootNode = factory.createElkNode();
rootNode.setIdentifier("root");
rootNode.setProperty(CoreOptions.ALGORITHM, "org.eclipse.elk.layered");
rootNode.setProperty(CoreOptions.DIRECTION, rootDirection);
rootNode.setProperty(CoreOptions.SPACING_NODE_NODE, NODE_SPACING);
rootNode.setProperty(CoreOptions.SPACING_EDGE_NODE, EDGE_SPACING);
rootNode.setProperty(CoreOptions.HIERARCHY_HANDLING, HierarchyHandling.INCLUDE_CHILDREN);
// 2. Partition graph nodes into main flow vs handler sections
List<RouteNode> mainNodes = new ArrayList<>();
List<RouteNode> handlerNodes = new ArrayList<>();
partitionNodes(graph, nodeById, mainNodes, handlerNodes);
// Build index of all RouteNodes (flat list from graph + recursive children)
Map<String, RouteNode> routeNodeMap = new HashMap<>();
if (graph.getNodes() != null) {
for (RouteNode rn : graph.getNodes()) {
indexNodeRecursive(rn, routeNodeMap);
// 3. Build ELK graphs — main flow + separate handler roots
ElkNode rootNode = createElkRoot("root", rootDirection, 1.0, ctx);
for (RouteNode rn : mainNodes) {
if (!ctx.elkNodeMap.containsKey(rn.getId())) {
createElkNodeRecursive(rn, rootNode, ctx);
}
}
// Track which nodes are children of a compound (at any depth)
Set<String> childNodeIds = new HashSet<>();
// Create ELK nodes recursively — compounds contain their children
Map<String, ElkNode> elkNodeMap = new HashMap<>();
Map<String, Color> nodeColors = new HashMap<>();
Set<String> compoundNodeIds = new HashSet<>();
// Process top-level nodes from the graph
if (graph.getNodes() != null) {
for (RouteNode rn : graph.getNodes()) {
if (!elkNodeMap.containsKey(rn.getId())) {
createElkNodeRecursive(rn, rootNode, factory, elkNodeMap, nodeColors,
compoundNodeIds, childNodeIds);
}
}
List<ElkNode> handlerRoots = new ArrayList<>();
for (RouteNode rn : handlerNodes) {
ElkNode hr = createElkRoot("handler-root-" + rn.getId(), rootDirection, 0.5, ctx);
createElkNodeRecursive(rn, hr, ctx);
handlerRoots.add(hr);
}
// Create ELK edges
if (graph.getEdges() != null) {
for (RouteEdge re : graph.getEdges()) {
ElkNode sourceElk = elkNodeMap.get(re.getSource());
ElkNode targetElk = elkNodeMap.get(re.getTarget());
if (sourceElk == null || targetElk == null) {
continue;
}
// 4. Create ELK edges (filtering DO_TRY internals and cross-root edges)
createElkEdges(graph, ctx);
// Determine the containing node for the edge
ElkNode containingNode = findCommonParent(sourceElk, targetElk);
ElkEdge elkEdge = factory.createElkEdge();
elkEdge.setContainingNode(containingNode);
elkEdge.getSources().add(sourceElk);
elkEdge.getTargets().add(targetElk);
}
}
// Run layout
// 5. Run layout engine
RecursiveGraphLayoutEngine engine = new RecursiveGraphLayoutEngine();
engine.layout(rootNode, new BasicProgressMonitor());
for (ElkNode hr : handlerRoots) {
engine.layout(hr, new BasicProgressMonitor());
}
// Extract results — only top-level nodes (children collected recursively)
List<PositionedNode> positionedNodes = new ArrayList<>();
Map<String, CompoundInfo> compoundInfos = new HashMap<>();
// 6. Post-process: fix DO_TRY section ordering and widths
postProcessDoTrySections(ctx);
// 7. Extract positioned result
return extractLayout(graph, rootNode, handlerRoots, ctx);
}
/**
* Build a node lookup by walking the root tree (which preserves children for
* compound nodes). Falls back to the flat nodes list for any nodes not in the
* tree (backward compatibility when root is not set).
*/
private Map<String, RouteNode> buildNodeIndex(RouteGraph graph) {
Map<String, RouteNode> nodeById = new HashMap<>();
if (graph.getRoot() != null) {
collectNodesRecursive(graph.getRoot(), nodeById);
}
if (graph.getNodes() != null) {
for (RouteNode rn : graph.getNodes()) {
if (childNodeIds.contains(rn.getId())) {
// Skip — collected under its parent compound
continue;
}
ElkNode elkNode = elkNodeMap.get(rn.getId());
if (elkNode == null) continue;
nodeById.putIfAbsent(rn.getId(), rn);
}
}
return nodeById;
}
positionedNodes.add(extractPositionedNode(rn, elkNode, elkNodeMap,
compoundNodeIds, compoundInfos, rootNode));
/** Recursively collect RouteNodes from the tree into a map (preserves children). */
private void collectNodesRecursive(RouteNode node, Map<String, RouteNode> map) {
if (node.getId() != null) {
map.put(node.getId(), node);
}
if (node.getChildren() != null) {
for (RouteNode child : node.getChildren()) {
collectNodesRecursive(child, map);
}
}
}
/**
* Separate main flow nodes from handler sections by walking FLOW edges
* from the graph root. Handler sections (onException, onCompletion, etc.)
* are placed in their own list for independent layout.
*/
private void partitionNodes(RouteGraph graph, Map<String, RouteNode> nodeById,
List<RouteNode> mainNodes, List<RouteNode> handlerNodes) {
Set<String> mainNodeIds = new HashSet<>();
if (graph.getRoot() != null && graph.getEdges() != null) {
mainNodeIds.add(graph.getRoot().getId());
boolean changed = true;
while (changed) {
changed = false;
for (RouteEdge re : graph.getEdges()) {
if (re.getEdgeType() == RouteEdge.EdgeType.ERROR) continue;
if (mainNodeIds.contains(re.getSource()) && !mainNodeIds.contains(re.getTarget())) {
RouteNode target = nodeById.get(re.getTarget());
if (target != null && target.getType() != null
&& HANDLER_SECTION_TYPES.contains(target.getType())) {
continue;
}
mainNodeIds.add(re.getTarget());
changed = true;
}
}
}
}
// Extract edges
Set<String> seen = new HashSet<>();
for (RouteNode rn : nodeById.values()) {
if (seen.contains(rn.getId())) continue;
seen.add(rn.getId());
if (rn.getType() != null && HANDLER_SECTION_TYPES.contains(rn.getType())
&& rn.getChildren() != null && !rn.getChildren().isEmpty()) {
handlerNodes.add(rn);
} else if (mainNodeIds.isEmpty() || mainNodeIds.contains(rn.getId())) {
mainNodes.add(rn);
}
}
}
/** Create a configured ELK root node for the layered algorithm. */
private ElkNode createElkRoot(String identifier, Direction direction,
double spacingScale, LayoutContext ctx) {
ElkNode root = ctx.factory.createElkNode();
root.setIdentifier(identifier);
root.setProperty(CoreOptions.ALGORITHM, "org.eclipse.elk.layered");
root.setProperty(CoreOptions.DIRECTION, direction);
root.setProperty(CoreOptions.SPACING_NODE_NODE, NODE_SPACING * spacingScale);
root.setProperty(CoreOptions.SPACING_EDGE_NODE, EDGE_SPACING * spacingScale);
root.setProperty(CoreOptions.HIERARCHY_HANDLING, HierarchyHandling.INCLUDE_CHILDREN);
root.setProperty(CoreOptions.EDGE_ROUTING, EdgeRouting.POLYLINE);
root.setProperty(org.eclipse.elk.alg.layered.options.LayeredOptions.NODE_PLACEMENT_STRATEGY,
NodePlacementStrategy.NETWORK_SIMPLEX);
return root;
}
/** Create ELK edges, skipping structural compound-to-child edges and cross-root edges. */
private void createElkEdges(RouteGraph graph, LayoutContext ctx) {
if (graph.getEdges() == null) return;
for (RouteEdge re : graph.getEdges()) {
ElkNode sourceElk = ctx.elkNodeMap.get(re.getSource());
ElkNode targetElk = ctx.elkNodeMap.get(re.getTarget());
if (sourceElk == null || targetElk == null) continue;
// Skip edges from any compound node to its own descendants
// (these are structural edges, not flow edges)
if (ctx.compoundNodeIds.contains(re.getSource()) && isDescendantOf(targetElk, sourceElk)) {
continue;
}
// Skip edges that cross ELK root boundaries — but allow continuation
// edges that exit a compound (source inside compound, target outside)
if (getElkRoot(sourceElk) != getElkRoot(targetElk)) {
// Allow if source and target share the same grandparent (main root)
// i.e., one is inside a compound and the other is a sibling
ElkNode sourceRoot = getElkRoot(sourceElk);
ElkNode targetRoot = getElkRoot(targetElk);
boolean sameGrandparent = sourceRoot.getParent() != null
&& targetRoot.getParent() != null
&& sourceRoot.getParent() == targetRoot.getParent();
boolean sourceExitsCompound = sourceRoot.getParent() != null
&& targetRoot == sourceRoot.getParent();
boolean targetExitsCompound = targetRoot.getParent() != null
&& sourceRoot == targetRoot.getParent();
if (!sameGrandparent && !sourceExitsCompound && !targetExitsCompound) continue;
}
ElkEdge elkEdge = ctx.factory.createElkEdge();
elkEdge.setContainingNode(findCommonParent(sourceElk, targetElk));
elkEdge.getSources().add(sourceElk);
elkEdge.getTargets().add(targetElk);
}
}
/**
* Post-process DO_TRY compounds after layout: re-stack sections in correct
* vertical order (try_body → doFinally → doCatch) and stretch to uniform width.
* ELK doesn't reliably order disconnected children within a compound.
*/
private void postProcessDoTrySections(LayoutContext ctx) {
for (Map.Entry<String, List<String>> entry : ctx.doTrySectionOrder.entrySet()) {
String doTryId = entry.getKey();
List<String> orderedIds = entry.getValue();
List<ElkNode> sections = new ArrayList<>();
for (String id : orderedIds) {
ElkNode section = ctx.elkNodeMap.get(id);
if (section != null) sections.add(section);
}
if (sections.size() < 2) continue;
// Left-align all sections and stack vertically
double startY = sections.stream().mapToDouble(ElkNode::getY).min().orElse(0);
double spacing = 20;
double currentY = startY;
for (ElkNode section : sections) {
section.setX(COMPOUND_SIDE_PADDING);
section.setY(currentY);
currentY += section.getHeight() + spacing;
}
// Uniform width
double maxWidth = sections.stream().mapToDouble(ElkNode::getWidth).max().orElse(0);
for (ElkNode section : sections) {
section.setWidth(maxWidth);
}
// Shrink DO_TRY parent height to fit its content
ElkNode doTryNode = ctx.elkNodeMap.get(doTryId);
if (doTryNode != null) {
double contentBottom = currentY - spacing + COMPOUND_SIDE_PADDING;
doTryNode.setHeight(contentBottom);
doTryNode.setWidth(maxWidth + 2 * COMPOUND_SIDE_PADDING);
}
}
}
/** Extract positioned nodes, edges, and bounding box from the ELK layout result. */
private LayoutResult extractLayout(RouteGraph graph, ElkNode rootNode,
List<ElkNode> handlerRoots, LayoutContext ctx) {
// Extract positioned nodes (uses tree-aware nodeById for compound children)
List<PositionedNode> positionedNodes = new ArrayList<>();
for (RouteNode rn : ctx.nodeById.values()) {
if (ctx.childNodeIds.contains(rn.getId())) continue;
ElkNode elkNode = ctx.elkNodeMap.get(rn.getId());
if (elkNode == null) continue;
positionedNodes.add(extractPositionedNode(rn, elkNode, getElkRoot(elkNode), ctx));
}
// Extract edges from main root + handler roots
List<PositionedEdge> positionedEdges = new ArrayList<>();
for (ElkEdge elkEdge : collectAllEdges(rootNode)) {
List<ElkEdge> allEdges = collectAllEdges(rootNode);
for (ElkNode hr : handlerRoots) {
allEdges.addAll(collectAllEdges(hr));
}
for (ElkEdge elkEdge : allEdges) {
String sourceId = elkEdge.getSources().isEmpty() ? "" : elkEdge.getSources().get(0).getIdentifier();
String targetId = elkEdge.getTargets().isEmpty() ? "" : elkEdge.getTargets().get(0).getIdentifier();
ElkNode containingNode = elkEdge.getContainingNode();
ElkNode edgeRoot = containingNode != null ? getElkRoot(containingNode) : null;
List<double[]> points = new ArrayList<>();
for (ElkEdgeSection section : elkEdge.getSections()) {
points.add(new double[]{
section.getStartX() + getAbsoluteX(elkEdge.getContainingNode(), rootNode),
section.getStartY() + getAbsoluteY(elkEdge.getContainingNode(), rootNode)
});
double cx = containingNode != null ? getAbsoluteX(containingNode, edgeRoot) : 0;
double cy = containingNode != null ? getAbsoluteY(containingNode, edgeRoot) : 0;
points.add(new double[]{section.getStartX() + cx, section.getStartY() + cy});
for (ElkBendPoint bp : section.getBendPoints()) {
points.add(new double[]{
bp.getX() + getAbsoluteX(elkEdge.getContainingNode(), rootNode),
bp.getY() + getAbsoluteY(elkEdge.getContainingNode(), rootNode)
});
points.add(new double[]{bp.getX() + cx, bp.getY() + cy});
}
points.add(new double[]{
section.getEndX() + getAbsoluteX(elkEdge.getContainingNode(), rootNode),
section.getEndY() + getAbsoluteY(elkEdge.getContainingNode(), rootNode)
});
points.add(new double[]{section.getEndX() + cx, section.getEndY() + cy});
}
// Find label from original edge
String label = "";
if (graph.getEdges() != null) {
for (RouteEdge re : graph.getEdges()) {
@@ -283,15 +438,24 @@ public class ElkDiagramRenderer implements DiagramRenderer {
}
}
}
positionedEdges.add(new PositionedEdge(sourceId, targetId, label, points));
}
double totalWidth = rootNode.getWidth();
double totalHeight = rootNode.getHeight();
// Compute bounding box
double totalWidth = 0, totalHeight = 0;
for (PositionedNode pn : allNodes(positionedNodes)) {
totalWidth = Math.max(totalWidth, pn.x() + pn.width());
totalHeight = Math.max(totalHeight, pn.y() + pn.height());
}
for (PositionedEdge pe : positionedEdges) {
for (double[] pt : pe.points()) {
totalWidth = Math.max(totalWidth, pt[0]);
totalHeight = Math.max(totalHeight, pt[1]);
}
}
DiagramLayout layout = new DiagramLayout(totalWidth, totalHeight, positionedNodes, positionedEdges);
return new LayoutResult(layout, nodeColors, compoundInfos);
return new LayoutResult(layout, ctx.nodeColors, ctx.compoundInfos);
}
// ----------------------------------------------------------------
@@ -316,7 +480,7 @@ public class ElkDiagramRenderer implements DiagramRenderer {
private void drawCompoundContainer(SVGGraphics2D g2, PositionedNode node, Color color) {
// Semi-transparent background
Color bg = new Color(color.getRed(), color.getGreen(), color.getBlue(), 38); // ~15% alpha
Color bg = new Color(color.getRed(), color.getGreen(), color.getBlue(), 38);
g2.setColor(bg);
g2.fill(new RoundRectangle2D.Double(
node.x(), node.y(), node.width(), node.height(),
@@ -331,7 +495,6 @@ public class ElkDiagramRenderer implements DiagramRenderer {
// Label at top
g2.setColor(color);
FontMetrics fm = g2.getFontMetrics();
float labelX = (float) (node.x() + COMPOUND_SIDE_PADDING);
float labelY = (float) (node.y() + 18);
g2.drawString(node.label(), labelX, labelY);
@@ -385,7 +548,7 @@ public class ElkDiagramRenderer implements DiagramRenderer {
if (ENDPOINT_TYPES.contains(type)) return BLUE;
if (PROCESSOR_TYPES.contains(type)) return GREEN;
if (ERROR_TYPES.contains(type)) return RED;
if (EIP_TYPES.contains(type)) return EIP_TYPES.contains(type) ? PURPLE : PURPLE;
if (EIP_TYPES.contains(type)) return PURPLE;
if (CROSS_ROUTE_TYPES.contains(type)) return CYAN;
return PURPLE;
}
@@ -403,34 +566,160 @@ public class ElkDiagramRenderer implements DiagramRenderer {
// Recursive node building
// ----------------------------------------------------------------
/** Index a RouteNode and all its descendants into the map. */
private void indexNodeRecursive(RouteNode node, Map<String, RouteNode> map) {
map.put(node.getId(), node);
if (node.getChildren() != null) {
for (RouteNode child : node.getChildren()) {
indexNodeRecursive(child, map);
}
}
}
/**
* Recursively create ELK nodes. Compound nodes become ELK containers
* with their children nested inside. Non-compound nodes become leaf nodes.
*/
private void createElkNodeRecursive(
RouteNode rn, ElkNode parentElk, ElkGraphFactory factory,
Map<String, ElkNode> elkNodeMap, Map<String, Color> nodeColors,
Set<String> compoundNodeIds, Set<String> childNodeIds) {
RouteNode rn, ElkNode parentElk, LayoutContext ctx) {
boolean isCompound = rn.getType() != null && COMPOUND_TYPES.contains(rn.getType())
&& rn.getChildren() != null && !rn.getChildren().isEmpty();
ElkNode elkNode = factory.createElkNode();
ElkNode elkNode = ctx.factory.createElkNode();
elkNode.setIdentifier(rn.getId());
elkNode.setParent(parentElk);
if (isCompound) {
compoundNodeIds.add(rn.getId());
if (isCompound && rn.getType() == NodeType.DO_TRY) {
// DO_TRY: vertical container with a virtual _TRY_BODY wrapper for the try body
// and DO_CATCH/DO_FINALLY as separate children below
ctx.doTryNodeIds.add(rn.getId());
ctx.compoundNodeIds.add(rn.getId());
elkNode.setWidth(200);
elkNode.setHeight(100);
elkNode.setProperty(CoreOptions.ALGORITHM, "org.eclipse.elk.layered");
elkNode.setProperty(CoreOptions.DIRECTION, Direction.DOWN);
elkNode.setProperty(CoreOptions.SPACING_NODE_NODE, NODE_SPACING * 0.4);
elkNode.setProperty(CoreOptions.SPACING_EDGE_NODE, EDGE_SPACING * 0.3);
elkNode.setProperty(CoreOptions.PADDING,
new org.eclipse.elk.core.math.ElkPadding(COMPOUND_TOP_PADDING,
COMPOUND_SIDE_PADDING, COMPOUND_SIDE_PADDING, COMPOUND_SIDE_PADDING));
// Separate try body children from handler children
List<RouteNode> tryBodyChildren = new ArrayList<>();
List<RouteNode> handlerChildren = new ArrayList<>();
for (RouteNode child : rn.getChildren()) {
if (child.getType() == NodeType.DO_CATCH || child.getType() == NodeType.DO_FINALLY) {
handlerChildren.add(child);
} else {
tryBodyChildren.add(child);
}
}
// Track desired section order: try_body → doFinally → doCatch
List<String> sectionOrder = new ArrayList<>();
// Virtual _TRY_BODY wrapper
if (!tryBodyChildren.isEmpty()) {
String wrapperId = rn.getId() + "._try_body";
ElkNode wrapper = ctx.factory.createElkNode();
wrapper.setIdentifier(wrapperId);
wrapper.setParent(elkNode);
wrapper.setWidth(200);
wrapper.setHeight(40);
wrapper.setProperty(CoreOptions.ALGORITHM, "org.eclipse.elk.layered");
wrapper.setProperty(CoreOptions.DIRECTION, Direction.RIGHT);
wrapper.setProperty(CoreOptions.SPACING_NODE_NODE, NODE_SPACING * 0.5);
wrapper.setProperty(CoreOptions.SPACING_EDGE_NODE, EDGE_SPACING * 0.5);
wrapper.setProperty(CoreOptions.PADDING,
new org.eclipse.elk.core.math.ElkPadding(8, 8, 8, 8));
ctx.compoundNodeIds.add(wrapperId);
ctx.elkNodeMap.put(wrapperId, wrapper);
sectionOrder.add(wrapperId);
for (RouteNode child : tryBodyChildren) {
ctx.childNodeIds.add(child.getId());
createElkNodeRecursive(child, wrapper, ctx);
}
}
// Handler sections in order: DO_FINALLY (middle), then DO_CATCH (bottom)
for (RouteNode handler : orderedHandlerChildren(handlerChildren)) {
ctx.childNodeIds.add(handler.getId());
createElkNodeRecursive(handler, elkNode, ctx);
sectionOrder.add(handler.getId());
}
ctx.doTrySectionOrder.put(rn.getId(), sectionOrder);
} else if (isCompound && rn.getType() == NodeType.EIP_CIRCUIT_BREAKER) {
// CIRCUIT_BREAKER: vertical container with _CB_MAIN for main path
// and onFallback as a compound section below (like DO_TRY pattern)
ctx.compoundNodeIds.add(rn.getId());
elkNode.setWidth(200);
elkNode.setHeight(100);
elkNode.setProperty(CoreOptions.ALGORITHM, "org.eclipse.elk.layered");
elkNode.setProperty(CoreOptions.DIRECTION, Direction.DOWN);
elkNode.setProperty(CoreOptions.SPACING_NODE_NODE, NODE_SPACING * 0.4);
elkNode.setProperty(CoreOptions.SPACING_EDGE_NODE, EDGE_SPACING * 0.3);
elkNode.setProperty(CoreOptions.PADDING,
new org.eclipse.elk.core.math.ElkPadding(COMPOUND_TOP_PADDING,
COMPOUND_SIDE_PADDING, COMPOUND_SIDE_PADDING, COMPOUND_SIDE_PADDING));
// Separate main path children from onFallback child
List<RouteNode> mainChildren = new ArrayList<>();
RouteNode fallbackNode = null;
for (RouteNode child : rn.getChildren()) {
if ("onFallback".equals(child.getLabel())) {
fallbackNode = child;
} else {
mainChildren.add(child);
}
}
List<String> sectionOrder = new ArrayList<>();
// Virtual _CB_MAIN wrapper for main path (horizontal flow)
if (!mainChildren.isEmpty()) {
String wrapperId = rn.getId() + "._cb_main";
ElkNode wrapper = ctx.factory.createElkNode();
wrapper.setIdentifier(wrapperId);
wrapper.setParent(elkNode);
wrapper.setWidth(200);
wrapper.setHeight(40);
wrapper.setProperty(CoreOptions.ALGORITHM, "org.eclipse.elk.layered");
wrapper.setProperty(CoreOptions.DIRECTION, Direction.RIGHT);
wrapper.setProperty(CoreOptions.SPACING_NODE_NODE, NODE_SPACING * 0.5);
wrapper.setProperty(CoreOptions.SPACING_EDGE_NODE, EDGE_SPACING * 0.5);
wrapper.setProperty(CoreOptions.PADDING,
new org.eclipse.elk.core.math.ElkPadding(8, 8, 8, 8));
ctx.compoundNodeIds.add(wrapperId);
ctx.elkNodeMap.put(wrapperId, wrapper);
sectionOrder.add(wrapperId);
for (RouteNode child : mainChildren) {
ctx.childNodeIds.add(child.getId());
createElkNodeRecursive(child, wrapper, ctx);
}
}
// onFallback as compound section containing its children
if (fallbackNode != null) {
ctx.childNodeIds.add(fallbackNode.getId());
ElkNode fallbackElk = ctx.factory.createElkNode();
fallbackElk.setIdentifier(fallbackNode.getId());
fallbackElk.setParent(elkNode);
fallbackElk.setWidth(200);
fallbackElk.setHeight(40);
fallbackElk.setProperty(CoreOptions.ALGORITHM, "org.eclipse.elk.layered");
fallbackElk.setProperty(CoreOptions.DIRECTION, Direction.RIGHT);
fallbackElk.setProperty(CoreOptions.SPACING_NODE_NODE, NODE_SPACING * 0.5);
fallbackElk.setProperty(CoreOptions.SPACING_EDGE_NODE, EDGE_SPACING * 0.5);
fallbackElk.setProperty(CoreOptions.PADDING,
new org.eclipse.elk.core.math.ElkPadding(18, 8, 8, 8));
ctx.compoundNodeIds.add(fallbackNode.getId());
ctx.elkNodeMap.put(fallbackNode.getId(), fallbackElk);
sectionOrder.add(fallbackNode.getId());
if (fallbackNode.getChildren() != null) {
for (RouteNode child : fallbackNode.getChildren()) {
ctx.childNodeIds.add(child.getId());
createElkNodeRecursive(child, fallbackElk, ctx);
}
}
}
ctx.doTrySectionOrder.put(rn.getId(), sectionOrder);
} else if (isCompound) {
ctx.compoundNodeIds.add(rn.getId());
elkNode.setWidth(200);
elkNode.setHeight(100);
elkNode.setProperty(CoreOptions.ALGORITHM, "org.eclipse.elk.layered");
@@ -441,44 +730,120 @@ public class ElkDiagramRenderer implements DiagramRenderer {
new org.eclipse.elk.core.math.ElkPadding(COMPOUND_TOP_PADDING,
COMPOUND_SIDE_PADDING, COMPOUND_SIDE_PADDING, COMPOUND_SIDE_PADDING));
// Recursively create children inside this compound
for (RouteNode child : rn.getChildren()) {
childNodeIds.add(child.getId());
createElkNodeRecursive(child, elkNode, factory, elkNodeMap, nodeColors,
compoundNodeIds, childNodeIds);
ctx.childNodeIds.add(child.getId());
createElkNodeRecursive(child, elkNode, ctx);
}
} else {
elkNode.setWidth(NODE_WIDTH);
elkNode.setHeight(NODE_HEIGHT);
}
elkNodeMap.put(rn.getId(), elkNode);
nodeColors.put(rn.getId(), colorForType(rn.getType()));
ctx.elkNodeMap.put(rn.getId(), elkNode);
ctx.nodeColors.put(rn.getId(), colorForType(rn.getType()));
}
/**
* Recursively extract a PositionedNode from the ELK layout result.
* Compound nodes include their children with absolute coordinates.
* All coordinates are absolute (relative to the ELK root).
*/
private PositionedNode extractPositionedNode(
RouteNode rn, ElkNode elkNode, Map<String, ElkNode> elkNodeMap,
Set<String> compoundNodeIds, Map<String, CompoundInfo> compoundInfos,
ElkNode rootNode) {
RouteNode rn, ElkNode elkNode, ElkNode rootNode, LayoutContext ctx) {
double absX = getAbsoluteX(elkNode, rootNode);
double absY = getAbsoluteY(elkNode, rootNode);
List<PositionedNode> children = List.of();
if (compoundNodeIds.contains(rn.getId()) && rn.getChildren() != null) {
if (ctx.compoundNodeIds.contains(rn.getId()) && rn.getChildren() != null) {
children = new ArrayList<>();
for (RouteNode child : rn.getChildren()) {
ElkNode childElk = elkNodeMap.get(child.getId());
if (childElk != null) {
children.add(extractPositionedNode(child, childElk, elkNodeMap,
compoundNodeIds, compoundInfos, rootNode));
if (rn.getType() == NodeType.DO_TRY) {
// DO_TRY: extract virtual _TRY_BODY wrapper first, then handler children
String wrapperId = rn.getId() + "._try_body";
ElkNode wrapperElk = ctx.elkNodeMap.get(wrapperId);
if (wrapperElk != null) {
List<PositionedNode> wrapperChildren = new ArrayList<>();
for (RouteNode child : rn.getChildren()) {
if (child.getType() != NodeType.DO_CATCH && child.getType() != NodeType.DO_FINALLY) {
ElkNode childElk = ctx.elkNodeMap.get(child.getId());
if (childElk != null) {
wrapperChildren.add(extractPositionedNode(child, childElk, rootNode, ctx));
}
}
}
children.add(new PositionedNode(
wrapperId, "", "_TRY_BODY",
getAbsoluteX(wrapperElk, rootNode),
getAbsoluteY(wrapperElk, rootNode),
wrapperElk.getWidth(), wrapperElk.getHeight(),
wrapperChildren, null));
ctx.compoundInfos.put(wrapperId, new CompoundInfo(wrapperId, Color.WHITE));
}
// Handler children in order: DO_FINALLY first, then DO_CATCH
for (RouteNode handler : orderedHandlerChildren(rn.getChildren())) {
ElkNode childElk = ctx.elkNodeMap.get(handler.getId());
if (childElk != null) {
children.add(extractPositionedNode(handler, childElk, rootNode, ctx));
}
}
} else if (rn.getType() == NodeType.EIP_CIRCUIT_BREAKER) {
// CIRCUIT_BREAKER: extract _CB_MAIN wrapper, then onFallback section
String mainWrapperId = rn.getId() + "._cb_main";
ElkNode mainWrapperElk = ctx.elkNodeMap.get(mainWrapperId);
if (mainWrapperElk != null) {
List<PositionedNode> wrapperChildren = new ArrayList<>();
for (RouteNode child : rn.getChildren()) {
if (!"onFallback".equals(child.getLabel())) {
ElkNode childElk = ctx.elkNodeMap.get(child.getId());
if (childElk != null) {
wrapperChildren.add(extractPositionedNode(child, childElk, rootNode, ctx));
}
}
}
children.add(new PositionedNode(
mainWrapperId, "", "_CB_MAIN",
getAbsoluteX(mainWrapperElk, rootNode),
getAbsoluteY(mainWrapperElk, rootNode),
mainWrapperElk.getWidth(), mainWrapperElk.getHeight(),
wrapperChildren, null));
ctx.compoundInfos.put(mainWrapperId, new CompoundInfo(mainWrapperId, Color.WHITE));
}
// onFallback section with its children, type overridden to _CB_FALLBACK
for (RouteNode child : rn.getChildren()) {
if ("onFallback".equals(child.getLabel())) {
ElkNode childElk = ctx.elkNodeMap.get(child.getId());
if (childElk != null) {
List<PositionedNode> fallbackChildren = new ArrayList<>();
if (child.getChildren() != null) {
for (RouteNode fc : child.getChildren()) {
ElkNode fcElk = ctx.elkNodeMap.get(fc.getId());
if (fcElk != null) {
fallbackChildren.add(extractPositionedNode(fc, fcElk, rootNode, ctx));
}
}
}
children.add(new PositionedNode(
child.getId(),
child.getLabel() != null ? child.getLabel() : "",
"_CB_FALLBACK",
getAbsoluteX(childElk, rootNode),
getAbsoluteY(childElk, rootNode),
childElk.getWidth(), childElk.getHeight(),
fallbackChildren, null));
ctx.compoundInfos.put(child.getId(),
new CompoundInfo(child.getId(), colorForType(rn.getType())));
}
}
}
} else {
for (RouteNode child : rn.getChildren()) {
ElkNode childElk = ctx.elkNodeMap.get(child.getId());
if (childElk != null) {
children.add(extractPositionedNode(child, childElk, rootNode, ctx));
}
}
}
compoundInfos.put(rn.getId(), new CompoundInfo(rn.getId(), colorForType(rn.getType())));
ctx.compoundInfos.put(rn.getId(), new CompoundInfo(rn.getId(), colorForType(rn.getType())));
}
return new PositionedNode(
@@ -487,7 +852,8 @@ public class ElkDiagramRenderer implements DiagramRenderer {
rn.getType() != null ? rn.getType().name() : "UNKNOWN",
absX, absY,
elkNode.getWidth(), elkNode.getHeight(),
children
children,
rn.getEndpointUri()
);
}
@@ -495,19 +861,52 @@ public class ElkDiagramRenderer implements DiagramRenderer {
// ELK graph helpers
// ----------------------------------------------------------------
/** Return handler children in section order: DO_FINALLY first, then DO_CATCH. */
private static List<RouteNode> orderedHandlerChildren(List<RouteNode> children) {
List<RouteNode> ordered = new ArrayList<>();
for (RouteNode c : children) {
if (c.getType() == NodeType.DO_FINALLY) ordered.add(c);
}
for (RouteNode c : children) {
if (c.getType() == NodeType.DO_CATCH) ordered.add(c);
}
return ordered;
}
/** Check if 'child' is a descendant of 'ancestor' in the ELK node hierarchy. */
private boolean isDescendantOf(ElkNode child, ElkNode ancestor) {
ElkNode current = child.getParent();
while (current != null) {
if (current == ancestor) return true;
current = current.getParent();
}
return false;
}
private ElkNode getElkRoot(ElkNode node) {
ElkNode current = node;
while (current.getParent() != null) {
current = current.getParent();
}
return current;
}
/** Proper lowest common ancestor of two ELK nodes. */
private ElkNode findCommonParent(ElkNode a, ElkNode b) {
if (a.getParent() == b.getParent()) {
return a.getParent();
Set<ElkNode> ancestorsOfA = new HashSet<>();
ElkNode current = a;
while (current != null) {
ancestorsOfA.add(current);
current = current.getParent();
}
// If one is the parent of the other
if (a.getParent() != null && a.getParent() == b) return b;
if (b.getParent() != null && b.getParent() == a) return a;
// Default: root (grandparent)
ElkNode parent = a.getParent();
while (parent != null && parent.getParent() != null) {
parent = parent.getParent();
current = b;
while (current != null) {
if (ancestorsOfA.contains(current)) {
return current;
}
current = current.getParent();
}
return parent != null ? parent : a.getParent();
return getElkRoot(a);
}
private double getAbsoluteX(ElkNode node, ElkNode root) {
@@ -538,13 +937,19 @@ public class ElkDiagramRenderer implements DiagramRenderer {
return edges;
}
/** Recursively find a PositionedNode by ID. */
private PositionedNode findNode(List<PositionedNode> nodes, String id) {
for (PositionedNode n : nodes) {
if (n.id().equals(id)) return n;
if (n.children() != null) {
PositionedNode found = findNode(n.children(), id);
if (found != null) return found;
}
}
return null;
}
/** Recursively flatten a PositionedNode tree. */
private List<PositionedNode> allNodes(List<PositionedNode> nodes) {
List<PositionedNode> all = new ArrayList<>();
for (PositionedNode n : nodes) {
@@ -556,6 +961,25 @@ public class ElkDiagramRenderer implements DiagramRenderer {
return all;
}
/** Collect IDs of all RouteNode descendants (for handler separation). */
private void collectDescendantIds(List<RouteNode> nodes, Set<String> ids) {
for (RouteNode n : nodes) {
ids.add(n.getId());
if (n.getChildren() != null) {
collectDescendantIds(n.getChildren(), ids);
}
}
}
private void collectAllIds(PositionedNode node, Set<String> ids) {
ids.add(node.id());
if (node.children() != null) {
for (PositionedNode child : node.children()) {
collectAllIds(child, ids);
}
}
}
// ----------------------------------------------------------------
// Internal data classes
// ----------------------------------------------------------------
@@ -567,4 +991,22 @@ public class ElkDiagramRenderer implements DiagramRenderer {
) {}
private record CompoundInfo(String nodeId, Color color) {}
/** Mutable state accumulated during ELK graph construction and extraction. */
private static class LayoutContext {
final ElkGraphFactory factory;
final Map<String, RouteNode> nodeById;
final Map<String, ElkNode> elkNodeMap = new HashMap<>();
final Map<String, Color> nodeColors = new HashMap<>();
final Set<String> compoundNodeIds = new HashSet<>();
final Set<String> childNodeIds = new HashSet<>();
final Set<String> doTryNodeIds = new HashSet<>();
final Map<String, List<String>> doTrySectionOrder = new LinkedHashMap<>();
final Map<String, CompoundInfo> compoundInfos = new HashMap<>();
LayoutContext(ElkGraphFactory factory, Map<String, RouteNode> nodeById) {
this.factory = factory;
this.nodeById = nodeById;
}
}
}

View File

@@ -0,0 +1,54 @@
package com.cameleer3.server.app.dto;
import com.cameleer3.server.core.admin.AppSettings;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotNull;
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
@Schema(description = "Per-application dashboard settings")
public record AppSettingsRequest(
@NotNull @Min(1)
@Schema(description = "SLA duration threshold in milliseconds")
Integer slaThresholdMs,
@NotNull @Min(0) @Max(100)
@Schema(description = "Error rate % threshold for warning (yellow) health dot")
Double healthErrorWarn,
@NotNull @Min(0) @Max(100)
@Schema(description = "Error rate % threshold for critical (red) health dot")
Double healthErrorCrit,
@NotNull @Min(0) @Max(100)
@Schema(description = "SLA compliance % threshold for warning (yellow) health dot")
Double healthSlaWarn,
@NotNull @Min(0) @Max(100)
@Schema(description = "SLA compliance % threshold for critical (red) health dot")
Double healthSlaCrit
) {
public AppSettings toSettings(String appId) {
Instant now = Instant.now();
return new AppSettings(appId, slaThresholdMs, healthErrorWarn, healthErrorCrit,
healthSlaWarn, healthSlaCrit, now, now);
}
public List<String> validate() {
List<String> errors = new ArrayList<>();
if (healthErrorWarn != null && healthErrorCrit != null
&& healthErrorWarn > healthErrorCrit) {
errors.add("healthErrorWarn must be <= healthErrorCrit");
}
if (healthSlaWarn != null && healthSlaCrit != null
&& healthSlaWarn < healthSlaCrit) {
errors.add("healthSlaWarn must be >= healthSlaCrit (higher SLA = healthier)");
}
return errors;
}
}

View File

@@ -0,0 +1,18 @@
package com.cameleer3.server.app.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
import java.util.Map;
@Schema(description = "Request to replay an exchange on an agent")
public record ReplayRequest(
@NotNull @Schema(description = "Camel route ID to replay on")
String routeId,
@Schema(description = "Message body for the replayed exchange")
String body,
@Schema(description = "Message headers for the replayed exchange")
Map<String, String> headers,
@Schema(description = "Exchange ID of the original execution being replayed (for audit trail)")
String originalExchangeId
) {}

View File

@@ -0,0 +1,13 @@
package com.cameleer3.server.app.dto;
import io.swagger.v3.oas.annotations.media.Schema;
@Schema(description = "Result of a replay command")
public record ReplayResponse(
@Schema(description = "Replay outcome: SUCCESS or FAILURE")
String status,
@Schema(description = "Human-readable result message")
String message,
@Schema(description = "Structured result data from the agent (JSON)")
String data
) {}

View File

@@ -15,5 +15,6 @@ public record RouteMetrics(
@NotNull double p99DurationMs,
@NotNull double errorRate,
@NotNull double throughputPerSec,
@NotNull List<Double> sparkline
@NotNull List<Double> sparkline,
double slaCompliance
) {}

View File

@@ -9,5 +9,7 @@ import java.time.Instant;
public record RouteSummary(
@NotNull String routeId,
@NotNull long exchangeCount,
Instant lastSeen
Instant lastSeen,
@Schema(description = "The from() endpoint URI, e.g. 'direct:processOrder'")
String fromEndpointUri
) {}

View File

@@ -179,8 +179,20 @@ public class OpenSearchIndex implements SearchIndex {
}
// Keyword filters (use .keyword sub-field for exact matching on dynamically mapped text fields)
if (request.status() != null)
filter.add(termQuery("status.keyword", request.status()));
if (request.status() != null && !request.status().isBlank()) {
String[] statuses = request.status().split(",");
if (statuses.length == 1) {
filter.add(termQuery("status.keyword", statuses[0].trim()));
} else {
filter.add(Query.of(q -> q.terms(t -> t
.field("status.keyword")
.terms(tv -> tv.value(
java.util.Arrays.stream(statuses)
.map(String::trim)
.map(FieldValue::of)
.toList())))));
}
}
if (request.routeId() != null)
filter.add(termQuery("route_id.keyword", request.routeId()));
if (request.agentId() != null)
@@ -349,6 +361,8 @@ public class OpenSearchIndex implements SearchIndex {
return pm;
}).toList());
}
map.put("has_trace_data", doc.hasTraceData());
map.put("is_replay", doc.isReplay());
return map;
}
@@ -385,7 +399,9 @@ public class OpenSearchIndex implements SearchIndex {
(String) src.get("error_message"),
null, // diagramContentHash not stored in index
extractHighlight(hit),
attributes
attributes,
Boolean.TRUE.equals(src.get("has_trace_data")),
Boolean.TRUE.equals(src.get("is_replay"))
);
}

View File

@@ -72,6 +72,7 @@ public class SecurityConfig {
.requestMatchers(HttpMethod.POST, "/api/v1/agents/*/commands").hasAnyRole("OPERATOR", "ADMIN")
.requestMatchers(HttpMethod.POST, "/api/v1/agents/groups/*/commands").hasAnyRole("OPERATOR", "ADMIN")
.requestMatchers(HttpMethod.POST, "/api/v1/agents/commands").hasAnyRole("OPERATOR", "ADMIN")
.requestMatchers(HttpMethod.POST, "/api/v1/agents/*/replay").hasAnyRole("OPERATOR", "ADMIN")
// Search endpoints
.requestMatchers(HttpMethod.GET, "/api/v1/search/**").hasAnyRole("VIEWER", "OPERATOR", "ADMIN", "AGENT")

View File

@@ -0,0 +1,67 @@
package com.cameleer3.server.app.storage;
import com.cameleer3.server.core.admin.AppSettings;
import com.cameleer3.server.core.admin.AppSettingsRepository;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.stereotype.Repository;
import java.util.List;
import java.util.Optional;
@Repository
public class PostgresAppSettingsRepository implements AppSettingsRepository {
private final JdbcTemplate jdbc;
private static final RowMapper<AppSettings> ROW_MAPPER = (rs, rowNum) -> new AppSettings(
rs.getString("app_id"),
rs.getInt("sla_threshold_ms"),
rs.getDouble("health_error_warn"),
rs.getDouble("health_error_crit"),
rs.getDouble("health_sla_warn"),
rs.getDouble("health_sla_crit"),
rs.getTimestamp("created_at").toInstant(),
rs.getTimestamp("updated_at").toInstant());
public PostgresAppSettingsRepository(JdbcTemplate jdbc) {
this.jdbc = jdbc;
}
@Override
public Optional<AppSettings> findByAppId(String appId) {
List<AppSettings> results = jdbc.query(
"SELECT * FROM app_settings WHERE app_id = ?", ROW_MAPPER, appId);
return results.isEmpty() ? Optional.empty() : Optional.of(results.get(0));
}
@Override
public List<AppSettings> findAll() {
return jdbc.query("SELECT * FROM app_settings ORDER BY app_id", ROW_MAPPER);
}
@Override
public AppSettings save(AppSettings settings) {
jdbc.update("""
INSERT INTO app_settings (app_id, sla_threshold_ms, health_error_warn,
health_error_crit, health_sla_warn, health_sla_crit, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, now(), now())
ON CONFLICT (app_id) DO UPDATE SET
sla_threshold_ms = EXCLUDED.sla_threshold_ms,
health_error_warn = EXCLUDED.health_error_warn,
health_error_crit = EXCLUDED.health_error_crit,
health_sla_warn = EXCLUDED.health_sla_warn,
health_sla_crit = EXCLUDED.health_sla_crit,
updated_at = now()
""",
settings.appId(), settings.slaThresholdMs(),
settings.healthErrorWarn(), settings.healthErrorCrit(),
settings.healthSlaWarn(), settings.healthSlaCrit());
return findByAppId(settings.appId()).orElseThrow();
}
@Override
public void delete(String appId) {
jdbc.update("DELETE FROM app_settings WHERE app_id = ?", appId);
}
}

View File

@@ -28,8 +28,13 @@ public class PostgresExecutionStore implements ExecutionStore {
status, correlation_id, exchange_id, start_time, end_time,
duration_ms, error_message, error_stacktrace, diagram_content_hash,
engine_level, input_body, output_body, input_headers, output_headers,
attributes, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?::jsonb, ?::jsonb, ?::jsonb, now(), now())
attributes,
error_type, error_category, root_cause_type, root_cause_message,
trace_id, span_id,
processors_json, has_trace_data, is_replay,
created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?::jsonb, ?::jsonb, ?::jsonb,
?, ?, ?, ?, ?, ?, ?::jsonb, ?, ?, now(), now())
ON CONFLICT (execution_id, start_time) DO UPDATE SET
status = CASE
WHEN EXCLUDED.status IN ('COMPLETED', 'FAILED')
@@ -49,6 +54,15 @@ public class PostgresExecutionStore implements ExecutionStore {
input_headers = COALESCE(EXCLUDED.input_headers, executions.input_headers),
output_headers = COALESCE(EXCLUDED.output_headers, executions.output_headers),
attributes = COALESCE(EXCLUDED.attributes, executions.attributes),
error_type = COALESCE(EXCLUDED.error_type, executions.error_type),
error_category = COALESCE(EXCLUDED.error_category, executions.error_category),
root_cause_type = COALESCE(EXCLUDED.root_cause_type, executions.root_cause_type),
root_cause_message = COALESCE(EXCLUDED.root_cause_message, executions.root_cause_message),
trace_id = COALESCE(EXCLUDED.trace_id, executions.trace_id),
span_id = COALESCE(EXCLUDED.span_id, executions.span_id),
processors_json = COALESCE(EXCLUDED.processors_json, executions.processors_json),
has_trace_data = EXCLUDED.has_trace_data OR executions.has_trace_data,
is_replay = EXCLUDED.is_replay OR executions.is_replay,
updated_at = now()
""",
execution.executionId(), execution.routeId(), execution.agentId(),
@@ -61,7 +75,11 @@ public class PostgresExecutionStore implements ExecutionStore {
execution.engineLevel(),
execution.inputBody(), execution.outputBody(),
execution.inputHeaders(), execution.outputHeaders(),
execution.attributes());
execution.attributes(),
execution.errorType(), execution.errorCategory(),
execution.rootCauseType(), execution.rootCauseMessage(),
execution.traceId(), execution.spanId(),
execution.processorsJson(), execution.hasTraceData(), execution.isReplay());
}
@Override
@@ -72,8 +90,13 @@ public class PostgresExecutionStore implements ExecutionStore {
INSERT INTO processor_executions (execution_id, processor_id, processor_type,
application_name, route_id, depth, parent_processor_id,
status, start_time, end_time, duration_ms, error_message, error_stacktrace,
input_body, output_body, input_headers, output_headers, attributes)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?::jsonb, ?::jsonb, ?::jsonb)
input_body, output_body, input_headers, output_headers, attributes,
loop_index, loop_size, split_index, split_size, multicast_index,
resolved_endpoint_uri,
error_type, error_category, root_cause_type, root_cause_message,
error_handler_type, circuit_breaker_state, fallback_triggered)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?::jsonb, ?::jsonb, ?::jsonb,
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT (execution_id, processor_id, start_time) DO UPDATE SET
status = EXCLUDED.status,
end_time = COALESCE(EXCLUDED.end_time, processor_executions.end_time),
@@ -84,7 +107,20 @@ public class PostgresExecutionStore implements ExecutionStore {
output_body = COALESCE(EXCLUDED.output_body, processor_executions.output_body),
input_headers = COALESCE(EXCLUDED.input_headers, processor_executions.input_headers),
output_headers = COALESCE(EXCLUDED.output_headers, processor_executions.output_headers),
attributes = COALESCE(EXCLUDED.attributes, processor_executions.attributes)
attributes = COALESCE(EXCLUDED.attributes, processor_executions.attributes),
loop_index = COALESCE(EXCLUDED.loop_index, processor_executions.loop_index),
loop_size = COALESCE(EXCLUDED.loop_size, processor_executions.loop_size),
split_index = COALESCE(EXCLUDED.split_index, processor_executions.split_index),
split_size = COALESCE(EXCLUDED.split_size, processor_executions.split_size),
multicast_index = COALESCE(EXCLUDED.multicast_index, processor_executions.multicast_index),
resolved_endpoint_uri = COALESCE(EXCLUDED.resolved_endpoint_uri, processor_executions.resolved_endpoint_uri),
error_type = COALESCE(EXCLUDED.error_type, processor_executions.error_type),
error_category = COALESCE(EXCLUDED.error_category, processor_executions.error_category),
root_cause_type = COALESCE(EXCLUDED.root_cause_type, processor_executions.root_cause_type),
root_cause_message = COALESCE(EXCLUDED.root_cause_message, processor_executions.root_cause_message),
error_handler_type = COALESCE(EXCLUDED.error_handler_type, processor_executions.error_handler_type),
circuit_breaker_state = COALESCE(EXCLUDED.circuit_breaker_state, processor_executions.circuit_breaker_state),
fallback_triggered = COALESCE(EXCLUDED.fallback_triggered, processor_executions.fallback_triggered)
""",
processors.stream().map(p -> new Object[]{
p.executionId(), p.processorId(), p.processorType(),
@@ -94,7 +130,14 @@ public class PostgresExecutionStore implements ExecutionStore {
p.endTime() != null ? Timestamp.from(p.endTime()) : null,
p.durationMs(), p.errorMessage(), p.errorStacktrace(),
p.inputBody(), p.outputBody(), p.inputHeaders(), p.outputHeaders(),
p.attributes()
p.attributes(),
p.loopIndex(), p.loopSize(), p.splitIndex(), p.splitSize(),
p.multicastIndex(),
p.resolvedEndpointUri(),
p.errorType(), p.errorCategory(),
p.rootCauseType(), p.rootCauseMessage(),
p.errorHandlerType(), p.circuitBreakerState(),
p.fallbackTriggered()
}).toList());
}
@@ -113,6 +156,13 @@ public class PostgresExecutionStore implements ExecutionStore {
PROCESSOR_MAPPER, executionId);
}
@Override
public Optional<ProcessorRecord> findProcessorById(String executionId, String processorId) {
String sql = "SELECT * FROM processor_executions WHERE execution_id = ? AND processor_id = ? LIMIT 1";
List<ProcessorRecord> results = jdbc.query(sql, PROCESSOR_MAPPER, executionId, processorId);
return results.isEmpty() ? Optional.empty() : Optional.of(results.get(0));
}
private static final RowMapper<ExecutionRecord> EXECUTION_MAPPER = (rs, rowNum) ->
new ExecutionRecord(
rs.getString("execution_id"), rs.getString("route_id"),
@@ -126,7 +176,13 @@ public class PostgresExecutionStore implements ExecutionStore {
rs.getString("engine_level"),
rs.getString("input_body"), rs.getString("output_body"),
rs.getString("input_headers"), rs.getString("output_headers"),
rs.getString("attributes"));
rs.getString("attributes"),
rs.getString("error_type"), rs.getString("error_category"),
rs.getString("root_cause_type"), rs.getString("root_cause_message"),
rs.getString("trace_id"), rs.getString("span_id"),
rs.getString("processors_json"),
rs.getBoolean("has_trace_data"),
rs.getBoolean("is_replay"));
private static final RowMapper<ProcessorRecord> PROCESSOR_MAPPER = (rs, rowNum) ->
new ProcessorRecord(
@@ -140,7 +196,17 @@ public class PostgresExecutionStore implements ExecutionStore {
rs.getString("error_message"), rs.getString("error_stacktrace"),
rs.getString("input_body"), rs.getString("output_body"),
rs.getString("input_headers"), rs.getString("output_headers"),
rs.getString("attributes"));
rs.getString("attributes"),
rs.getObject("loop_index") != null ? rs.getInt("loop_index") : null,
rs.getObject("loop_size") != null ? rs.getInt("loop_size") : null,
rs.getObject("split_index") != null ? rs.getInt("split_index") : null,
rs.getObject("split_size") != null ? rs.getInt("split_size") : null,
rs.getObject("multicast_index") != null ? rs.getInt("multicast_index") : null,
rs.getString("resolved_endpoint_uri"),
rs.getString("error_type"), rs.getString("error_category"),
rs.getString("root_cause_type"), rs.getString("root_cause_message"),
rs.getString("error_handler_type"), rs.getString("circuit_breaker_state"),
rs.getObject("fallback_triggered") != null ? rs.getBoolean("fallback_triggered") : null);
private static Instant toInstant(ResultSet rs, String column) throws SQLException {
Timestamp ts = rs.getTimestamp(column);

View File

@@ -3,6 +3,7 @@ package com.cameleer3.server.app.storage;
import com.cameleer3.server.core.search.ExecutionStats;
import com.cameleer3.server.core.search.StatsTimeseries;
import com.cameleer3.server.core.search.StatsTimeseries.TimeseriesBucket;
import com.cameleer3.server.core.search.TopError;
import com.cameleer3.server.core.storage.StatsStore;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;
@@ -12,7 +13,9 @@ import java.time.Duration;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
@Repository
public class PostgresStatsStore implements StatsStore {
@@ -184,4 +187,242 @@ public class PostgresStatsStore implements StatsStore {
return new StatsTimeseries(buckets);
}
// ── Grouped timeseries ────────────────────────────────────────────────
@Override
public Map<String, StatsTimeseries> timeseriesGroupedByApp(Instant from, Instant to, int bucketCount) {
return queryGroupedTimeseries("stats_1m_app", "application_name", from, to,
bucketCount, List.of());
}
@Override
public Map<String, StatsTimeseries> timeseriesGroupedByRoute(Instant from, Instant to,
int bucketCount, String applicationName) {
return queryGroupedTimeseries("stats_1m_route", "route_id", from, to,
bucketCount, List.of(new Filter("application_name", applicationName)));
}
private Map<String, StatsTimeseries> queryGroupedTimeseries(
String view, String groupCol, Instant from, Instant to,
int bucketCount, List<Filter> filters) {
long intervalSeconds = Duration.between(from, to).toSeconds() / Math.max(bucketCount, 1);
if (intervalSeconds < 60) intervalSeconds = 60;
String sql = "SELECT time_bucket(? * INTERVAL '1 second', bucket) AS period, " +
groupCol + " AS group_key, " +
"COALESCE(SUM(total_count), 0) AS total_count, " +
"COALESCE(SUM(failed_count), 0) AS failed_count, " +
"CASE WHEN SUM(total_count) > 0 THEN SUM(duration_sum) / SUM(total_count) ELSE 0 END AS avg_duration, " +
"COALESCE(MAX(p99_duration), 0) AS p99_duration, " +
"COALESCE(SUM(running_count), 0) AS active_count " +
"FROM " + view + " WHERE bucket >= ? AND bucket < ?";
List<Object> params = new ArrayList<>();
params.add(intervalSeconds);
params.add(Timestamp.from(from));
params.add(Timestamp.from(to));
for (Filter f : filters) {
sql += " AND " + f.column() + " = ?";
params.add(f.value());
}
sql += " GROUP BY period, group_key ORDER BY period, group_key";
Map<String, List<TimeseriesBucket>> grouped = new LinkedHashMap<>();
jdbc.query(sql, (rs) -> {
String key = rs.getString("group_key");
TimeseriesBucket bucket = new TimeseriesBucket(
rs.getTimestamp("period").toInstant(),
rs.getLong("total_count"), rs.getLong("failed_count"),
rs.getLong("avg_duration"), rs.getLong("p99_duration"),
rs.getLong("active_count"));
grouped.computeIfAbsent(key, k -> new ArrayList<>()).add(bucket);
}, params.toArray());
Map<String, StatsTimeseries> result = new LinkedHashMap<>();
grouped.forEach((key, buckets) -> result.put(key, new StatsTimeseries(buckets)));
return result;
}
// ── SLA compliance ────────────────────────────────────────────────────
@Override
public double slaCompliance(Instant from, Instant to, int thresholdMs,
String applicationName, String routeId) {
String sql = "SELECT " +
"COUNT(*) FILTER (WHERE duration_ms <= ? AND status != 'RUNNING') AS compliant, " +
"COUNT(*) FILTER (WHERE status != 'RUNNING') AS total " +
"FROM executions WHERE start_time >= ? AND start_time < ?";
List<Object> params = new ArrayList<>();
params.add(thresholdMs);
params.add(Timestamp.from(from));
params.add(Timestamp.from(to));
if (applicationName != null) {
sql += " AND application_name = ?";
params.add(applicationName);
}
if (routeId != null) {
sql += " AND route_id = ?";
params.add(routeId);
}
return jdbc.query(sql, (rs, rowNum) -> {
long total = rs.getLong("total");
if (total == 0) return 1.0;
return rs.getLong("compliant") * 100.0 / total;
}, params.toArray()).stream().findFirst().orElse(1.0);
}
@Override
public Map<String, long[]> slaCountsByApp(Instant from, Instant to, int defaultThresholdMs) {
String sql = "SELECT application_name, " +
"COUNT(*) FILTER (WHERE duration_ms <= ? AND status != 'RUNNING') AS compliant, " +
"COUNT(*) FILTER (WHERE status != 'RUNNING') AS total " +
"FROM executions WHERE start_time >= ? AND start_time < ? " +
"GROUP BY application_name";
Map<String, long[]> result = new LinkedHashMap<>();
jdbc.query(sql, (rs) -> {
result.put(rs.getString("application_name"),
new long[]{rs.getLong("compliant"), rs.getLong("total")});
}, defaultThresholdMs, Timestamp.from(from), Timestamp.from(to));
return result;
}
@Override
public Map<String, long[]> slaCountsByRoute(Instant from, Instant to,
String applicationName, int thresholdMs) {
String sql = "SELECT route_id, " +
"COUNT(*) FILTER (WHERE duration_ms <= ? AND status != 'RUNNING') AS compliant, " +
"COUNT(*) FILTER (WHERE status != 'RUNNING') AS total " +
"FROM executions WHERE start_time >= ? AND start_time < ? " +
"AND application_name = ? GROUP BY route_id";
Map<String, long[]> result = new LinkedHashMap<>();
jdbc.query(sql, (rs) -> {
result.put(rs.getString("route_id"),
new long[]{rs.getLong("compliant"), rs.getLong("total")});
}, thresholdMs, Timestamp.from(from), Timestamp.from(to), applicationName);
return result;
}
// ── Top errors ────────────────────────────────────────────────────────
@Override
public List<TopError> topErrors(Instant from, Instant to, String applicationName,
String routeId, int limit) {
StringBuilder where = new StringBuilder(
"status = 'FAILED' AND start_time >= ? AND start_time < ?");
List<Object> params = new ArrayList<>();
params.add(Timestamp.from(from));
params.add(Timestamp.from(to));
if (applicationName != null) {
where.append(" AND application_name = ?");
params.add(applicationName);
}
String table;
String groupId;
if (routeId != null) {
// L3: attribute errors to processors
table = "processor_executions";
groupId = "processor_id";
where.append(" AND route_id = ?");
params.add(routeId);
} else {
// L1/L2: attribute errors to routes
table = "executions";
groupId = "route_id";
}
Instant fiveMinAgo = Instant.now().minus(5, ChronoUnit.MINUTES);
Instant tenMinAgo = Instant.now().minus(10, ChronoUnit.MINUTES);
String sql = "WITH counted AS (" +
" SELECT COALESCE(error_type, LEFT(error_message, 200)) AS error_key, " +
" " + groupId + " AS group_id, " +
" COUNT(*) AS cnt, MAX(start_time) AS last_seen " +
" FROM " + table + " WHERE " + where +
" GROUP BY error_key, group_id ORDER BY cnt DESC LIMIT ?" +
"), velocity AS (" +
" SELECT COALESCE(error_type, LEFT(error_message, 200)) AS error_key, " +
" COUNT(*) FILTER (WHERE start_time >= ?) AS recent_5m, " +
" COUNT(*) FILTER (WHERE start_time >= ? AND start_time < ?) AS prev_5m " +
" FROM " + table + " WHERE " + where +
" GROUP BY error_key" +
") SELECT c.error_key, c.group_id, c.cnt, c.last_seen, " +
" COALESCE(v.recent_5m, 0) / 5.0 AS velocity, " +
" CASE " +
" WHEN COALESCE(v.recent_5m, 0) > COALESCE(v.prev_5m, 0) * 1.2 THEN 'accelerating' " +
" WHEN COALESCE(v.recent_5m, 0) < COALESCE(v.prev_5m, 0) * 0.8 THEN 'decelerating' " +
" ELSE 'stable' END AS trend " +
"FROM counted c LEFT JOIN velocity v ON c.error_key = v.error_key " +
"ORDER BY c.cnt DESC";
// Build full params: counted-where params + limit + velocity timestamps + velocity-where params
List<Object> fullParams = new ArrayList<>(params);
fullParams.add(limit);
fullParams.add(Timestamp.from(fiveMinAgo));
fullParams.add(Timestamp.from(tenMinAgo));
fullParams.add(Timestamp.from(fiveMinAgo));
fullParams.addAll(params); // same where clause for velocity CTE
return jdbc.query(sql, (rs, rowNum) -> {
String errorKey = rs.getString("error_key");
String gid = rs.getString("group_id");
return new TopError(
errorKey,
routeId != null ? routeId : gid, // routeId
routeId != null ? gid : null, // processorId (only at L3)
rs.getLong("cnt"),
rs.getDouble("velocity"),
rs.getString("trend"),
rs.getTimestamp("last_seen").toInstant());
}, fullParams.toArray());
}
@Override
public int activeErrorTypes(Instant from, Instant to, String applicationName) {
String sql = "SELECT COUNT(DISTINCT COALESCE(error_type, LEFT(error_message, 200))) " +
"FROM executions WHERE status = 'FAILED' AND start_time >= ? AND start_time < ?";
List<Object> params = new ArrayList<>();
params.add(Timestamp.from(from));
params.add(Timestamp.from(to));
if (applicationName != null) {
sql += " AND application_name = ?";
params.add(applicationName);
}
Integer count = jdbc.queryForObject(sql, Integer.class, params.toArray());
return count != null ? count : 0;
}
// ── Punchcard ─────────────────────────────────────────────────────────
@Override
public List<PunchcardCell> punchcard(Instant from, Instant to, String applicationName) {
String view = applicationName != null ? "stats_1m_app" : "stats_1m_all";
String sql = "SELECT EXTRACT(DOW FROM bucket) AS weekday, " +
"EXTRACT(HOUR FROM bucket) AS hour, " +
"COALESCE(SUM(total_count), 0) AS total_count, " +
"COALESCE(SUM(failed_count), 0) AS failed_count " +
"FROM " + view + " WHERE bucket >= ? AND bucket < ?";
List<Object> params = new ArrayList<>();
params.add(Timestamp.from(from));
params.add(Timestamp.from(to));
if (applicationName != null) {
sql += " AND application_name = ?";
params.add(applicationName);
}
sql += " GROUP BY weekday, hour ORDER BY weekday, hour";
return jdbc.query(sql, (rs, rowNum) -> new PunchcardCell(
rs.getInt("weekday"), rs.getInt("hour"),
rs.getLong("total_count"), rs.getLong("failed_count")),
params.toArray());
}
}

View File

@@ -16,9 +16,9 @@ public class SpaForwardController {
@GetMapping(value = {
"/login",
"/executions",
"/executions/{path:[^\\.]*}",
"/executions/**",
"/oidc/callback",
"/admin/{path:[^\\.]*}"
"/admin/**"
})
public String forward() {
return "forward:/index.html";

View File

@@ -0,0 +1,23 @@
-- executions: store raw processor tree for faithful detail response
ALTER TABLE executions ADD COLUMN processors_json JSONB;
-- executions: error categorization + OTel tracing
ALTER TABLE executions ADD COLUMN error_type TEXT;
ALTER TABLE executions ADD COLUMN error_category TEXT;
ALTER TABLE executions ADD COLUMN root_cause_type TEXT;
ALTER TABLE executions ADD COLUMN root_cause_message TEXT;
ALTER TABLE executions ADD COLUMN trace_id TEXT;
ALTER TABLE executions ADD COLUMN span_id TEXT;
-- processor_executions: error categorization + circuit breaker
ALTER TABLE processor_executions ADD COLUMN error_type TEXT;
ALTER TABLE processor_executions ADD COLUMN error_category TEXT;
ALTER TABLE processor_executions ADD COLUMN root_cause_type TEXT;
ALTER TABLE processor_executions ADD COLUMN root_cause_message TEXT;
ALTER TABLE processor_executions ADD COLUMN error_handler_type TEXT;
ALTER TABLE processor_executions ADD COLUMN circuit_breaker_state TEXT;
ALTER TABLE processor_executions ADD COLUMN fallback_triggered BOOLEAN;
-- Remove erroneous depth columns from V9
ALTER TABLE processor_executions DROP COLUMN IF EXISTS split_depth;
ALTER TABLE processor_executions DROP COLUMN IF EXISTS loop_depth;

View File

@@ -0,0 +1,10 @@
-- Flag indicating whether any processor in this execution captured trace data
ALTER TABLE executions ADD COLUMN IF NOT EXISTS has_trace_data BOOLEAN NOT NULL DEFAULT FALSE;
-- Backfill: set flag for existing executions that have processor trace data
UPDATE executions e SET has_trace_data = TRUE
WHERE EXISTS (
SELECT 1 FROM processor_executions pe
WHERE pe.execution_id = e.execution_id
AND (pe.input_body IS NOT NULL OR pe.output_body IS NOT NULL)
);

View File

@@ -0,0 +1,11 @@
-- Per-application dashboard settings (SLA thresholds, health dot thresholds)
CREATE TABLE app_settings (
app_id TEXT PRIMARY KEY,
sla_threshold_ms INTEGER NOT NULL DEFAULT 300,
health_error_warn DOUBLE PRECISION NOT NULL DEFAULT 1.0,
health_error_crit DOUBLE PRECISION NOT NULL DEFAULT 5.0,
health_sla_warn DOUBLE PRECISION NOT NULL DEFAULT 99.0,
health_sla_crit DOUBLE PRECISION NOT NULL DEFAULT 95.0,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

View File

@@ -0,0 +1,7 @@
-- Flag indicating whether this execution is a replayed exchange
ALTER TABLE executions ADD COLUMN IF NOT EXISTS is_replay BOOLEAN NOT NULL DEFAULT FALSE;
-- Backfill: check inputHeaders JSON for X-Cameleer-Replay header
UPDATE executions SET is_replay = TRUE
WHERE input_headers IS NOT NULL
AND input_headers::jsonb ? 'X-Cameleer-Replay';

View File

@@ -0,0 +1,5 @@
ALTER TABLE processor_executions ADD COLUMN IF NOT EXISTS loop_index INTEGER;
ALTER TABLE processor_executions ADD COLUMN IF NOT EXISTS loop_size INTEGER;
ALTER TABLE processor_executions ADD COLUMN IF NOT EXISTS split_index INTEGER;
ALTER TABLE processor_executions ADD COLUMN IF NOT EXISTS split_size INTEGER;
ALTER TABLE processor_executions ADD COLUMN IF NOT EXISTS multicast_index INTEGER;

View File

@@ -0,0 +1,3 @@
ALTER TABLE processor_executions ADD COLUMN resolved_endpoint_uri TEXT;
ALTER TABLE processor_executions ADD COLUMN split_depth INTEGER DEFAULT 0;
ALTER TABLE processor_executions ADD COLUMN loop_depth INTEGER DEFAULT 0;

View File

@@ -39,7 +39,8 @@ class ElkDiagramRendererTest {
RouteNode process = new RouteNode("node-2", NodeType.BEAN, "myProcessor");
RouteNode to = new RouteNode("node-3", NodeType.TO, "log:output");
graph.setNodes(List.of(from, process, to));
from.setChildren(List.of(process, to));
graph.setRoot(from);
graph.setEdges(List.of(
new RouteEdge("node-1", "node-2", RouteEdge.EdgeType.FLOW),
new RouteEdge("node-2", "node-3", RouteEdge.EdgeType.FLOW)
@@ -62,10 +63,10 @@ class ElkDiagramRendererTest {
RouteNode otherwise = new RouteNode("node-4", NodeType.EIP_OTHERWISE, "otherwise");
RouteNode to = new RouteNode("node-5", NodeType.TO, "log:result");
// Set children on the choice node
// Build tree: from → [choice, to]; choice → [when, otherwise]
choice.setChildren(List.of(when, otherwise));
graph.setNodes(List.of(from, choice, when, otherwise, to));
from.setChildren(List.of(choice, to));
graph.setRoot(from);
graph.setEdges(List.of(
new RouteEdge("node-1", "node-2", RouteEdge.EdgeType.FLOW),
new RouteEdge("node-2", "node-3", RouteEdge.EdgeType.FLOW),
@@ -172,6 +173,97 @@ class ElkDiagramRendererTest {
assertEquals(2, choiceNode.children().size(), "Choice node should have 2 children (when, otherwise)");
}
/**
* Build a DO_TRY graph: from -> doTry(try: [process, log], doFinally: [cleanup], doCatch: [errorLog]) -> to
*/
private RouteGraph buildDoTryGraph() {
RouteGraph graph = new RouteGraph("try-catch-route");
graph.setExtractedAt(Instant.now());
graph.setVersion(1);
RouteNode from = new RouteNode("node-1", NodeType.ENDPOINT, "timer:tick");
RouteNode doTry = new RouteNode("node-2", NodeType.DO_TRY, "doTry");
RouteNode process = new RouteNode("node-3", NodeType.PROCESSOR, "process");
RouteNode log1 = new RouteNode("node-4", NodeType.LOG, "log:tryBody");
RouteNode doFinally = new RouteNode("node-5", NodeType.DO_FINALLY, "doFinally");
RouteNode cleanup = new RouteNode("node-6", NodeType.LOG, "log:cleanup");
RouteNode doCatch = new RouteNode("node-7", NodeType.DO_CATCH, "doCatch");
RouteNode errorLog = new RouteNode("node-8", NodeType.LOG, "log:error");
RouteNode to = new RouteNode("node-9", NodeType.TO, "log:done");
doFinally.setChildren(List.of(cleanup));
doCatch.setChildren(List.of(errorLog));
doTry.setChildren(List.of(process, log1, doFinally, doCatch));
from.setChildren(List.of(doTry, to));
graph.setRoot(from);
graph.setEdges(List.of(
new RouteEdge("node-1", "node-2", RouteEdge.EdgeType.FLOW),
new RouteEdge("node-2", "node-3", RouteEdge.EdgeType.FLOW),
new RouteEdge("node-3", "node-4", RouteEdge.EdgeType.FLOW),
new RouteEdge("node-2", "node-5", RouteEdge.EdgeType.FLOW),
new RouteEdge("node-5", "node-6", RouteEdge.EdgeType.FLOW),
new RouteEdge("node-2", "node-7", RouteEdge.EdgeType.FLOW),
new RouteEdge("node-7", "node-8", RouteEdge.EdgeType.FLOW),
new RouteEdge("node-2", "node-9", RouteEdge.EdgeType.FLOW)
));
return graph;
}
@Test
void layoutJson_doTryGraph_sectionsInCorrectOrder() {
DiagramLayout layout = renderer.layoutJson(buildDoTryGraph());
assertNotNull(layout);
// Find the DO_TRY compound node
PositionedNode doTryNode = layout.nodes().stream()
.filter(n -> "node-2".equals(n.id()))
.findFirst()
.orElseThrow(() -> new AssertionError("DO_TRY node not found"));
assertNotNull(doTryNode.children(), "DO_TRY should have children");
assertFalse(doTryNode.children().isEmpty(), "DO_TRY should have non-empty children");
// Find sections by ID pattern
PositionedNode tryBody = doTryNode.children().stream()
.filter(n -> n.id() != null && n.id().contains("._try_body"))
.findFirst().orElse(null);
PositionedNode finallySection = doTryNode.children().stream()
.filter(n -> "node-5".equals(n.id()))
.findFirst().orElse(null);
PositionedNode catchSection = doTryNode.children().stream()
.filter(n -> "node-7".equals(n.id()))
.findFirst().orElse(null);
assertNotNull(tryBody, "Try body wrapper should exist");
assertNotNull(finallySection, "doFinally section should exist");
assertNotNull(catchSection, "doCatch section should exist");
// Verify vertical order: tryBody.y < doFinally.y < doCatch.y
assertTrue(tryBody.y() < finallySection.y(),
"Try body (y=" + tryBody.y() + ") should be above doFinally (y=" + finallySection.y() + ")");
assertTrue(finallySection.y() < catchSection.y(),
"doFinally (y=" + finallySection.y() + ") should be above doCatch (y=" + catchSection.y() + ")");
}
@Test
void layoutJson_doTryGraph_sectionsHaveSameWidth() {
DiagramLayout layout = renderer.layoutJson(buildDoTryGraph());
PositionedNode doTryNode = layout.nodes().stream()
.filter(n -> "node-2".equals(n.id()))
.findFirst()
.orElseThrow(() -> new AssertionError("DO_TRY node not found"));
List<PositionedNode> sections = doTryNode.children();
double firstWidth = sections.get(0).width();
for (PositionedNode section : sections) {
assertEquals(firstWidth, section.width(), 0.1,
"All sections should have the same width, but " + section.id() + " differs");
}
}
@Test
void renderSvg_compoundGraph_producesValidSvg() {
String svg = renderer.renderSvg(buildCompoundGraph());

View File

@@ -36,7 +36,7 @@ class OpenSearchIndexIT extends AbstractPostgresIT {
"OrderNotFoundException: order-12345 not found", null,
List.of(new ProcessorDoc("proc-1", "log", "COMPLETED",
null, null, "request body with customer-99", null, null, null, null)),
null);
null, false, false);
searchIndex.index(doc);
refreshOpenSearchIndices();
@@ -62,7 +62,7 @@ class OpenSearchIndexIT extends AbstractPostgresIT {
now, now.plusMillis(50), 50L, null, null,
List.of(new ProcessorDoc("proc-1", "bean", "COMPLETED",
null, null, "UniquePayloadIdentifier12345", null, null, null, null)),
null);
null, false, false);
searchIndex.index(doc);
refreshOpenSearchIndices();

View File

@@ -26,7 +26,8 @@ class PostgresExecutionStoreIT extends AbstractPostgresIT {
"COMPLETED", "corr-1", "exchange-1",
now, now.plusMillis(100), 100L,
null, null, null,
"REGULAR", null, null, null, null, null);
"REGULAR", null, null, null, null, null,
null, null, null, null, null, null, null, false, false);
executionStore.upsert(record);
Optional<ExecutionRecord> found = executionStore.findById("exec-1");
@@ -43,11 +44,13 @@ class PostgresExecutionStoreIT extends AbstractPostgresIT {
ExecutionRecord first = new ExecutionRecord(
"exec-dup", "route-a", "agent-1", "app-1",
"RUNNING", null, null, now, null, null, null, null, null,
null, null, null, null, null, null);
null, null, null, null, null, null,
null, null, null, null, null, null, null, false, false);
ExecutionRecord second = new ExecutionRecord(
"exec-dup", "route-a", "agent-1", "app-1",
"COMPLETED", null, null, now, now.plusMillis(200), 200L, null, null, null,
"COMPLETE", null, null, null, null, null);
"COMPLETE", null, null, null, null, null,
null, null, null, null, null, null, null, false, false);
executionStore.upsert(first);
executionStore.upsert(second);
@@ -64,18 +67,23 @@ class PostgresExecutionStoreIT extends AbstractPostgresIT {
ExecutionRecord exec = new ExecutionRecord(
"exec-proc", "route-a", "agent-1", "app-1",
"COMPLETED", null, null, now, now.plusMillis(50), 50L, null, null, null,
"COMPLETE", null, null, null, null, null);
"COMPLETE", null, null, null, null, null,
null, null, null, null, null, null, null, false, false);
executionStore.upsert(exec);
List<ProcessorRecord> processors = List.of(
new ProcessorRecord("exec-proc", "proc-1", "log",
"app-1", "route-a", 0, null, "COMPLETED",
now, now.plusMillis(10), 10L, null, null,
"input body", "output body", null, null, null),
"input body", "output body", null, null, null,
null, null, null, null, null,
null, null, null, null, null, null, null, null),
new ProcessorRecord("exec-proc", "proc-2", "to",
"app-1", "route-a", 1, "proc-1", "COMPLETED",
now.plusMillis(10), now.plusMillis(30), 20L, null, null,
null, null, null, null, null)
null, null, null, null, null,
null, null, null, null, null,
null, null, null, null, null, null, null, null)
);
executionStore.upsertProcessors("exec-proc", now, "app-1", "route-a", processors);

View File

@@ -60,6 +60,7 @@ class PostgresStatsStoreIT extends AbstractPostgresIT {
id, routeId, "agent-1", applicationName, status, null, null,
startTime, startTime.plusMillis(durationMs), durationMs,
status.equals("FAILED") ? "error" : null, null, null,
null, null, null, null, null, null));
null, null, null, null, null, null,
null, null, null, null, null, null, null, false, false));
}
}

View File

@@ -0,0 +1,19 @@
package com.cameleer3.server.core.admin;
import java.time.Instant;
public record AppSettings(
String appId,
int slaThresholdMs,
double healthErrorWarn,
double healthErrorCrit,
double healthSlaWarn,
double healthSlaCrit,
Instant createdAt,
Instant updatedAt) {
public static AppSettings defaults(String appId) {
Instant now = Instant.now();
return new AppSettings(appId, 300, 1.0, 5.0, 99.0, 95.0, now, now);
}
}

View File

@@ -0,0 +1,11 @@
package com.cameleer3.server.core.admin;
import java.util.List;
import java.util.Optional;
public interface AppSettingsRepository {
Optional<AppSettings> findByAppId(String appId);
List<AppSettings> findAll();
AppSettings save(AppSettings settings);
void delete(String appId);
}

View File

@@ -8,5 +8,6 @@ public enum CommandType {
DEEP_TRACE,
REPLAY,
SET_TRACED_PROCESSORS,
TEST_EXPRESSION
TEST_EXPRESSION,
ROUTE_CONTROL
}

View File

@@ -1,16 +1,21 @@
package com.cameleer3.server.core.detail;
import com.cameleer3.common.model.ProcessorExecution;
import com.cameleer3.server.core.storage.ExecutionStore;
import com.cameleer3.server.core.storage.ExecutionStore.ProcessorRecord;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.util.*;
public class DetailService {
private static final ObjectMapper JSON = new ObjectMapper();
private static final ObjectMapper JSON = new ObjectMapper()
.findAndRegisterModules()
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
private static final TypeReference<Map<String, String>> STR_MAP = new TypeReference<>() {};
private static final TypeReference<List<ProcessorExecution>> PROCESSOR_EXEC_LIST = new TypeReference<>() {};
private final ExecutionStore executionStore;
@@ -21,8 +26,14 @@ public class DetailService {
public Optional<ExecutionDetail> getDetail(String executionId) {
return executionStore.findById(executionId)
.map(exec -> {
List<ProcessorRecord> processors = executionStore.findProcessors(executionId);
List<ProcessorNode> roots = buildTree(processors);
// Prefer the raw processor tree (faithful to agent data) over
// flat-record reconstruction (which loses iteration context).
List<ProcessorNode> processors = parseProcessorsJson(exec.processorsJson());
if (processors == null) {
// Fallback for executions ingested before processors_json was added
List<ProcessorRecord> records = executionStore.findProcessors(executionId);
processors = buildTree(records);
}
return new ExecutionDetail(
exec.executionId(), exec.routeId(), exec.agentId(),
exec.applicationName(),
@@ -30,25 +41,100 @@ public class DetailService {
exec.durationMs() != null ? exec.durationMs() : 0L,
exec.correlationId(), exec.exchangeId(),
exec.errorMessage(), exec.errorStacktrace(),
exec.diagramContentHash(), roots,
exec.diagramContentHash(), processors,
exec.inputBody(), exec.outputBody(),
exec.inputHeaders(), exec.outputHeaders(),
parseAttributes(exec.attributes())
parseAttributes(exec.attributes()),
exec.errorType(), exec.errorCategory(),
exec.rootCauseType(), exec.rootCauseMessage(),
exec.traceId(), exec.spanId()
);
});
}
public Optional<Map<String, String>> getProcessorSnapshot(String executionId, String processorId) {
return executionStore.findProcessorById(executionId, processorId)
.map(p -> {
Map<String, String> snapshot = new LinkedHashMap<>();
if (p.inputBody() != null) snapshot.put("inputBody", p.inputBody());
if (p.outputBody() != null) snapshot.put("outputBody", p.outputBody());
if (p.inputHeaders() != null) snapshot.put("inputHeaders", p.inputHeaders());
if (p.outputHeaders() != null) snapshot.put("outputHeaders", p.outputHeaders());
return snapshot;
});
}
/** Parse the raw processor tree JSON stored alongside the execution. */
private List<ProcessorNode> parseProcessorsJson(String json) {
if (json == null || json.isBlank()) return null;
try {
List<ProcessorExecution> executions = JSON.readValue(json, PROCESSOR_EXEC_LIST);
return convertProcessors(executions);
} catch (Exception e) {
return null;
}
}
/** Convert agent ProcessorExecution tree to detail ProcessorNode tree. */
private List<ProcessorNode> convertProcessors(List<ProcessorExecution> executions) {
if (executions == null) return List.of();
List<ProcessorNode> result = new ArrayList<>();
for (ProcessorExecution p : executions) {
boolean hasTrace = p.getInputBody() != null || p.getOutputBody() != null
|| p.getInputHeaders() != null || p.getOutputHeaders() != null;
ProcessorNode node = new ProcessorNode(
p.getProcessorId(), p.getProcessorType(),
p.getStatus() != null ? p.getStatus().name() : null,
p.getStartTime(), p.getEndTime(),
p.getDurationMs(),
p.getErrorMessage(), p.getErrorStackTrace(),
p.getAttributes() != null ? new LinkedHashMap<>(p.getAttributes()) : null,
p.getLoopIndex(), p.getLoopSize(),
p.getSplitIndex(), p.getSplitSize(),
p.getMulticastIndex(),
p.getResolvedEndpointUri(),
p.getErrorType(), p.getErrorCategory(),
p.getRootCauseType(), p.getRootCauseMessage(),
p.getErrorHandlerType(), p.getCircuitBreakerState(),
p.getFallbackTriggered(),
p.getFilterMatched(), p.getDuplicateMessage(),
hasTrace
);
for (ProcessorNode child : convertProcessors(p.getChildren())) {
node.addChild(child);
}
result.add(node);
}
return result;
}
/**
* Fallback: reconstruct processor tree from flat records.
* Note: this loses iteration context for processors with the same ID across iterations.
*/
List<ProcessorNode> buildTree(List<ProcessorRecord> processors) {
if (processors.isEmpty()) return List.of();
Map<String, ProcessorNode> nodeMap = new LinkedHashMap<>();
for (ProcessorRecord p : processors) {
boolean hasTrace = p.inputBody() != null || p.outputBody() != null
|| p.inputHeaders() != null || p.outputHeaders() != null;
nodeMap.put(p.processorId(), new ProcessorNode(
p.processorId(), p.processorType(), p.status(),
p.startTime(), p.endTime(),
p.durationMs() != null ? p.durationMs() : 0L,
p.errorMessage(), p.errorStacktrace(),
parseAttributes(p.attributes())
parseAttributes(p.attributes()),
p.loopIndex(), p.loopSize(),
p.splitIndex(), p.splitSize(),
p.multicastIndex(),
p.resolvedEndpointUri(),
p.errorType(), p.errorCategory(),
p.rootCauseType(), p.rootCauseMessage(),
p.errorHandlerType(), p.circuitBreakerState(),
p.fallbackTriggered(),
null, null, // filterMatched, duplicateMessage (not in flat DB records)
hasTrace
));
}

View File

@@ -47,6 +47,12 @@ public record ExecutionDetail(
String outputBody,
String inputHeaders,
String outputHeaders,
Map<String, String> attributes
Map<String, String> attributes,
String errorType,
String errorCategory,
String rootCauseType,
String rootCauseMessage,
String traceId,
String spanId
) {
}

View File

@@ -22,12 +22,38 @@ public final class ProcessorNode {
private final String errorMessage;
private final String errorStackTrace;
private final Map<String, String> attributes;
private final Integer loopIndex;
private final Integer loopSize;
private final Integer splitIndex;
private final Integer splitSize;
private final Integer multicastIndex;
private final String resolvedEndpointUri;
private final String errorType;
private final String errorCategory;
private final String rootCauseType;
private final String rootCauseMessage;
private final String errorHandlerType;
private final String circuitBreakerState;
private final Boolean fallbackTriggered;
private final Boolean filterMatched;
private final Boolean duplicateMessage;
private final boolean hasTraceData;
private final List<ProcessorNode> children;
public ProcessorNode(String processorId, String processorType, String status,
Instant startTime, Instant endTime, long durationMs,
String errorMessage, String errorStackTrace,
Map<String, String> attributes) {
Map<String, String> attributes,
Integer loopIndex, Integer loopSize,
Integer splitIndex, Integer splitSize,
Integer multicastIndex,
String resolvedEndpointUri,
String errorType, String errorCategory,
String rootCauseType, String rootCauseMessage,
String errorHandlerType, String circuitBreakerState,
Boolean fallbackTriggered,
Boolean filterMatched, Boolean duplicateMessage,
boolean hasTraceData) {
this.processorId = processorId;
this.processorType = processorType;
this.status = status;
@@ -37,6 +63,22 @@ public final class ProcessorNode {
this.errorMessage = errorMessage;
this.errorStackTrace = errorStackTrace;
this.attributes = attributes;
this.loopIndex = loopIndex;
this.loopSize = loopSize;
this.splitIndex = splitIndex;
this.splitSize = splitSize;
this.multicastIndex = multicastIndex;
this.resolvedEndpointUri = resolvedEndpointUri;
this.errorType = errorType;
this.errorCategory = errorCategory;
this.rootCauseType = rootCauseType;
this.rootCauseMessage = rootCauseMessage;
this.errorHandlerType = errorHandlerType;
this.circuitBreakerState = circuitBreakerState;
this.fallbackTriggered = fallbackTriggered;
this.filterMatched = filterMatched;
this.duplicateMessage = duplicateMessage;
this.hasTraceData = hasTraceData;
this.children = new ArrayList<>();
}
@@ -53,5 +95,21 @@ public final class ProcessorNode {
public String getErrorMessage() { return errorMessage; }
public String getErrorStackTrace() { return errorStackTrace; }
public Map<String, String> getAttributes() { return attributes; }
public Integer getLoopIndex() { return loopIndex; }
public Integer getLoopSize() { return loopSize; }
public Integer getSplitIndex() { return splitIndex; }
public Integer getSplitSize() { return splitSize; }
public Integer getMulticastIndex() { return multicastIndex; }
public String getResolvedEndpointUri() { return resolvedEndpointUri; }
public String getErrorType() { return errorType; }
public String getErrorCategory() { return errorCategory; }
public String getRootCauseType() { return rootCauseType; }
public String getRootCauseMessage() { return rootCauseMessage; }
public String getErrorHandlerType() { return errorHandlerType; }
public String getCircuitBreakerState() { return circuitBreakerState; }
public Boolean getFallbackTriggered() { return fallbackTriggered; }
public Boolean getFilterMatched() { return filterMatched; }
public Boolean getDuplicateMessage() { return duplicateMessage; }
public boolean isHasTraceData() { return hasTraceData; }
public List<ProcessorNode> getChildren() { return List.copyOf(children); }
}

View File

@@ -8,14 +8,15 @@ import java.util.List;
* For compound nodes (CHOICE, SPLIT, TRY_CATCH, etc.), {@code children}
* contains the nested child nodes rendered inside the parent bounds.
*
* @param id node identifier (matches RouteNode.id)
* @param label display label
* @param type NodeType name (e.g., "ENDPOINT", "PROCESSOR")
* @param x horizontal position
* @param y vertical position
* @param width node width
* @param height node height
* @param children nested child nodes for compound/swimlane groups
* @param id node identifier (matches RouteNode.id)
* @param label display label
* @param type NodeType name (e.g., "ENDPOINT", "PROCESSOR")
* @param x horizontal position
* @param y vertical position
* @param width node width
* @param height node height
* @param children nested child nodes for compound/swimlane groups
* @param endpointUri the Camel endpoint URI (e.g., "direct:processOrder"), null for non-endpoint nodes
*/
public record PositionedNode(
String id,
@@ -25,6 +26,7 @@ public record PositionedNode(
double y,
double width,
double height,
List<PositionedNode> children
List<PositionedNode> children,
String endpointUri
) {
}

View File

@@ -79,7 +79,7 @@ public class SearchIndexer implements SearchIndexerStats {
exec.status(), exec.correlationId(), exec.exchangeId(),
exec.startTime(), exec.endTime(), exec.durationMs(),
exec.errorMessage(), exec.errorStacktrace(), processorDocs,
exec.attributes()));
exec.attributes(), exec.hasTraceData(), exec.isReplay()));
indexedCount.incrementAndGet();
lastIndexedAt = Instant.now();

View File

@@ -3,6 +3,7 @@ package com.cameleer3.server.core.ingestion;
import com.cameleer3.common.model.ExchangeSnapshot;
import com.cameleer3.common.model.ProcessorExecution;
import com.cameleer3.common.model.RouteExecution;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.cameleer3.server.core.indexing.ExecutionUpdatedEvent;
import com.cameleer3.server.core.storage.DiagramStore;
import com.cameleer3.server.core.storage.ExecutionStore;
@@ -19,7 +20,9 @@ import java.util.function.Consumer;
public class IngestionService {
private static final ObjectMapper JSON = new ObjectMapper();
private static final ObjectMapper JSON = new ObjectMapper()
.findAndRegisterModules()
.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
private final ExecutionStore executionStore;
private final DiagramStore diagramStore;
@@ -97,6 +100,14 @@ public class IngestionService {
outputHeaders = toJson(outputSnapshot.getHeaders());
}
boolean hasTraceData = hasAnyTraceData(exec.getProcessors());
boolean isReplay = exec.getReplayExchangeId() != null;
if (!isReplay && inputSnapshot != null && inputSnapshot.getHeaders() != null) {
isReplay = "true".equalsIgnoreCase(
String.valueOf(inputSnapshot.getHeaders().get("X-Cameleer-Replay")));
}
return new ExecutionRecord(
exec.getExchangeId(), exec.getRouteId(), agentId, applicationName,
exec.getStatus() != null ? exec.getStatus().name() : "RUNNING",
@@ -107,10 +118,25 @@ public class IngestionService {
diagramHash,
exec.getEngineLevel(),
inputBody, outputBody, inputHeaders, outputHeaders,
toJson(exec.getAttributes())
toJson(exec.getAttributes()),
exec.getErrorType(), exec.getErrorCategory(),
exec.getRootCauseType(), exec.getRootCauseMessage(),
exec.getTraceId(), exec.getSpanId(),
toJsonObject(exec.getProcessors()),
hasTraceData,
isReplay
);
}
private static boolean hasAnyTraceData(List<ProcessorExecution> processors) {
if (processors == null) return false;
for (ProcessorExecution p : processors) {
if (p.getInputBody() != null || p.getOutputBody() != null) return true;
if (hasAnyTraceData(p.getChildren())) return true;
}
return false;
}
private List<ProcessorRecord> flattenProcessors(
List<ProcessorExecution> processors, String executionId,
java.time.Instant execStartTime, String applicationName, String routeId,
@@ -128,7 +154,15 @@ public class IngestionService {
p.getErrorMessage(), p.getErrorStackTrace(),
truncateBody(p.getInputBody()), truncateBody(p.getOutputBody()),
toJson(p.getInputHeaders()), toJson(p.getOutputHeaders()),
toJson(p.getAttributes())
toJson(p.getAttributes()),
p.getLoopIndex(), p.getLoopSize(),
p.getSplitIndex(), p.getSplitSize(),
p.getMulticastIndex(),
p.getResolvedEndpointUri(),
p.getErrorType(), p.getErrorCategory(),
p.getRootCauseType(), p.getRootCauseMessage(),
p.getErrorHandlerType(), p.getCircuitBreakerState(),
p.getFallbackTriggered()
));
if (p.getChildren() != null) {
flat.addAll(flattenProcessors(
@@ -153,4 +187,13 @@ public class IngestionService {
return "{}";
}
}
private static String toJsonObject(Object obj) {
if (obj == null) return null;
try {
return JSON.writeValueAsString(obj);
} catch (JsonProcessingException e) {
return null;
}
}
}

View File

@@ -1,5 +1,8 @@
package com.cameleer3.server.core.ingestion;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ArrayBlockingQueue;
@@ -16,6 +19,8 @@ import java.util.concurrent.BlockingQueue;
*/
public class WriteBuffer<T> {
private static final Logger log = LoggerFactory.getLogger(WriteBuffer.class);
private final BlockingQueue<T> queue;
private final int capacity;
@@ -45,7 +50,10 @@ public class WriteBuffer<T> {
return false;
}
for (T item : items) {
queue.offer(item);
if (!queue.offer(item)) {
log.warn("WriteBuffer offer rejected despite capacity check — possible concurrent modification");
return false;
}
}
return true;
}

View File

@@ -14,4 +14,23 @@ public record ExecutionStats(
long prevTotalCount,
long prevFailedCount,
long prevAvgDurationMs,
long prevP99LatencyMs) {}
long prevP99LatencyMs,
double slaCompliance) {
/** Constructor without SLA compliance (backward-compatible, sets to -1). */
public ExecutionStats(long totalCount, long failedCount, long avgDurationMs,
long p99LatencyMs, long activeCount, long totalToday,
long prevTotalCount, long prevFailedCount,
long prevAvgDurationMs, long prevP99LatencyMs) {
this(totalCount, failedCount, avgDurationMs, p99LatencyMs, activeCount,
totalToday, prevTotalCount, prevFailedCount, prevAvgDurationMs,
prevP99LatencyMs, -1.0);
}
/** Return a copy with the given SLA compliance value. */
public ExecutionStats withSlaCompliance(double slaCompliance) {
return new ExecutionStats(totalCount, failedCount, avgDurationMs, p99LatencyMs,
activeCount, totalToday, prevTotalCount, prevFailedCount,
prevAvgDurationMs, prevP99LatencyMs, slaCompliance);
}
}

View File

@@ -33,6 +33,8 @@ public record ExecutionSummary(
String errorMessage,
String diagramContentHash,
String highlight,
Map<String, String> attributes
Map<String, String> attributes,
boolean hasTraceData,
boolean isReplay
) {
}

View File

@@ -5,6 +5,7 @@ import com.cameleer3.server.core.storage.StatsStore;
import java.time.Instant;
import java.util.List;
import java.util.Map;
public class SearchService {
@@ -48,4 +49,42 @@ public class SearchService {
String routeId, List<String> agentIds) {
return statsStore.timeseriesForRoute(from, to, bucketCount, routeId, agentIds);
}
// ── Dashboard-specific queries ────────────────────────────────────────
public Map<String, StatsTimeseries> timeseriesGroupedByApp(Instant from, Instant to, int bucketCount) {
return statsStore.timeseriesGroupedByApp(from, to, bucketCount);
}
public Map<String, StatsTimeseries> timeseriesGroupedByRoute(Instant from, Instant to,
int bucketCount, String applicationName) {
return statsStore.timeseriesGroupedByRoute(from, to, bucketCount, applicationName);
}
public double slaCompliance(Instant from, Instant to, int thresholdMs,
String applicationName, String routeId) {
return statsStore.slaCompliance(from, to, thresholdMs, applicationName, routeId);
}
public Map<String, long[]> slaCountsByApp(Instant from, Instant to, int defaultThresholdMs) {
return statsStore.slaCountsByApp(from, to, defaultThresholdMs);
}
public Map<String, long[]> slaCountsByRoute(Instant from, Instant to,
String applicationName, int thresholdMs) {
return statsStore.slaCountsByRoute(from, to, applicationName, thresholdMs);
}
public List<TopError> topErrors(Instant from, Instant to, String applicationName,
String routeId, int limit) {
return statsStore.topErrors(from, to, applicationName, routeId, limit);
}
public int activeErrorTypes(Instant from, Instant to, String applicationName) {
return statsStore.activeErrorTypes(from, to, applicationName);
}
public List<StatsStore.PunchcardCell> punchcard(Instant from, Instant to, String applicationName) {
return statsStore.punchcard(from, to, applicationName);
}
}

View File

@@ -0,0 +1,12 @@
package com.cameleer3.server.core.search;
import java.time.Instant;
public record TopError(
String errorType,
String routeId,
String processorId,
long count,
double velocity,
String trend,
Instant lastSeen) {}

View File

@@ -16,6 +16,8 @@ public interface ExecutionStore {
List<ProcessorRecord> findProcessors(String executionId);
Optional<ProcessorRecord> findProcessorById(String executionId, String processorId);
record ExecutionRecord(
String executionId, String routeId, String agentId, String applicationName,
String status, String correlationId, String exchangeId,
@@ -23,7 +25,13 @@ public interface ExecutionStore {
String errorMessage, String errorStacktrace, String diagramContentHash,
String engineLevel,
String inputBody, String outputBody, String inputHeaders, String outputHeaders,
String attributes
String attributes,
String errorType, String errorCategory,
String rootCauseType, String rootCauseMessage,
String traceId, String spanId,
String processorsJson,
boolean hasTraceData,
boolean isReplay
) {}
record ProcessorRecord(
@@ -33,6 +41,14 @@ public interface ExecutionStore {
Instant startTime, Instant endTime, Long durationMs,
String errorMessage, String errorStacktrace,
String inputBody, String outputBody, String inputHeaders, String outputHeaders,
String attributes
String attributes,
Integer loopIndex, Integer loopSize,
Integer splitIndex, Integer splitSize,
Integer multicastIndex,
String resolvedEndpointUri,
String errorType, String errorCategory,
String rootCauseType, String rootCauseMessage,
String errorHandlerType, String circuitBreakerState,
Boolean fallbackTriggered
) {}
}

View File

@@ -2,9 +2,11 @@ package com.cameleer3.server.core.storage;
import com.cameleer3.server.core.search.ExecutionStats;
import com.cameleer3.server.core.search.StatsTimeseries;
import com.cameleer3.server.core.search.TopError;
import java.time.Instant;
import java.util.List;
import java.util.Map;
public interface StatsStore {
@@ -33,4 +35,34 @@ public interface StatsStore {
// Per-processor timeseries
StatsTimeseries timeseriesForProcessor(Instant from, Instant to, int bucketCount,
String routeId, String processorType);
// Grouped timeseries by application (for L1 dashboard charts)
Map<String, StatsTimeseries> timeseriesGroupedByApp(Instant from, Instant to, int bucketCount);
// Grouped timeseries by route within an application (for L2 dashboard charts)
Map<String, StatsTimeseries> timeseriesGroupedByRoute(Instant from, Instant to, int bucketCount,
String applicationName);
// SLA compliance: % of completed exchanges with duration <= thresholdMs
double slaCompliance(Instant from, Instant to, int thresholdMs,
String applicationName, String routeId);
// Batch SLA counts by app: {appId -> [compliant, total]}
Map<String, long[]> slaCountsByApp(Instant from, Instant to, int defaultThresholdMs);
// Batch SLA counts by route within an app: {routeId -> [compliant, total]}
Map<String, long[]> slaCountsByRoute(Instant from, Instant to, String applicationName,
int thresholdMs);
// Top N errors with velocity trend
List<TopError> topErrors(Instant from, Instant to, String applicationName,
String routeId, int limit);
// Count of distinct error types in window
int activeErrorTypes(Instant from, Instant to, String applicationName);
// Punchcard: aggregate by weekday (0=Sun..6=Sat) x hour (0-23) over last 7 days
List<PunchcardCell> punchcard(Instant from, Instant to, String applicationName);
record PunchcardCell(int weekday, int hour, long totalCount, long failedCount) {}
}

View File

@@ -9,7 +9,9 @@ public record ExecutionDocument(
Instant startTime, Instant endTime, Long durationMs,
String errorMessage, String errorStacktrace,
List<ProcessorDoc> processors,
String attributes
String attributes,
boolean hasTraceData,
boolean isReplay
) {
public record ProcessorDoc(
String processorId, String processorType, String status,

View File

@@ -27,7 +27,9 @@ class TreeReconstructionTest {
"exec-1", id, type,
"default", "route1", depth, parentId,
status, NOW, NOW, 10L,
null, null, null, null, null, null, null
null, null, null, null, null, null, null,
null, null, null, null, null,
null, null, null, null, null, null, null, null
);
}

402
ci-log.txt Normal file
View File

@@ -0,0 +1,402 @@
2026-03-25T12:05:26.5603617Z my-docker-runner(version:v0.3.0) received task 1104 of job build, be triggered by event: push
2026-03-25T12:05:26.5608954Z workflow prepared
2026-03-25T12:05:26.5610069Z evaluating expression 'github.event_name != 'delete''
2026-03-25T12:05:26.5611215Z expression 'github.event_name != 'delete'' evaluated to 'true'
2026-03-25T12:05:26.5611555Z 🚀 Start image=maven:3.9-eclipse-temurin-17
2026-03-25T12:05:26.5687706Z 🐳 docker pull image=maven:3.9-eclipse-temurin-17 platform= username= forcePull=true
2026-03-25T12:05:26.5688071Z 🐳 docker pull maven:3.9-eclipse-temurin-17
2026-03-25T12:05:26.5688457Z pulling image 'docker.io/library/maven:3.9-eclipse-temurin-17' ()
2026-03-25T12:05:27.4255898Z Pulling from library/maven :: 3.9-eclipse-temurin-17
2026-03-25T12:05:27.4496622Z Digest: sha256:39a5260d49fe20e5f407bf63f63a267d9870965bcd1d114e52e1e50ba1c55a32 ::
2026-03-25T12:05:27.4497205Z Status: Image is up to date for maven:3.9-eclipse-temurin-17 ::
2026-03-25T12:05:27.4684745Z 🐳 docker create image=maven:3.9-eclipse-temurin-17 platform= entrypoint=["/bin/sleep" "10800"] cmd=[] network="gitea_gitea_default"
2026-03-25T12:05:27.4853152Z Custom container.Config from options ==> &{Hostname: Domainname: User: AttachStdin:false AttachStdout:true AttachStderr:true ExposedPorts:map[] Tty:false OpenStdin:false StdinOnce:false Env:[] Cmd:[] Healthcheck:<nil> ArgsEscaped:false Image: Volumes:map[] WorkingDir: Entrypoint:[] NetworkDisabled:false MacAddress: OnBuild:[] Labels:map[] StopSignal: StopTimeout:<nil> Shell:[]}
2026-03-25T12:05:27.4854116Z Merged container.Config ==> &{Hostname: Domainname: User: AttachStdin:false AttachStdout:true AttachStderr:true ExposedPorts:map[] Tty:false OpenStdin:false StdinOnce:false Env:[RUNNER_TOOL_CACHE=/opt/hostedtoolcache RUNNER_OS=Linux RUNNER_ARCH=ARM64 RUNNER_TEMP=/tmp LANG=C.UTF-8] Cmd:[] Healthcheck:<nil> ArgsEscaped:false Image:maven:3.9-eclipse-temurin-17 Volumes:map[] WorkingDir:/workspace/***/***3-server Entrypoint:[/bin/sleep 10800] NetworkDisabled:false MacAddress: OnBuild:[] Labels:map[] StopSignal: StopTimeout:<nil> Shell:[]}
2026-03-25T12:05:27.4855205Z Custom container.HostConfig from options ==> &{Binds:[] ContainerIDFile: LogConfig:{Type: Config:map[]} NetworkMode:gitea_gitea_default PortBindings:map[] RestartPolicy:{Name:no MaximumRetryCount:0} AutoRemove:false VolumeDriver: VolumesFrom:[] ConsoleSize:[0 0] Annotations:map[] CapAdd:[] CapDrop:[] CgroupnsMode: DNS:[] DNSOptions:[] DNSSearch:[] ExtraHosts:[] GroupAdd:[] IpcMode: Cgroup: Links:[] OomScoreAdj:0 PidMode: Privileged:false PublishAllPorts:false ReadonlyRootfs:false SecurityOpt:[] StorageOpt:map[] Tmpfs:map[] UTSMode: UsernsMode: ShmSize:0 Sysctls:map[] Runtime: Isolation: Resources:{CPUShares:0 Memory:0 NanoCPUs:0 CgroupParent: BlkioWeight:0 BlkioWeightDevice:[] BlkioDeviceReadBps:[] BlkioDeviceWriteBps:[] BlkioDeviceReadIOps:[] BlkioDeviceWriteIOps:[] CPUPeriod:0 CPUQuota:0 CPURealtimePeriod:0 CPURealtimeRuntime:0 CpusetCpus: CpusetMems: Devices:[] DeviceCgroupRules:[] DeviceRequests:[] KernelMemory:0 KernelMemoryTCP:0 MemoryReservation:0 MemorySwap:0 MemorySwappiness:0x228ace413ac8 OomKillDisable:0x228ace4139c3 PidsLimit:0x228ace413b28 Ulimits:[] CPUCount:0 CPUPercent:0 IOMaximumIOps:0 IOMaximumBandwidth:0} Mounts:[] MaskedPaths:[] ReadonlyPaths:[] Init:<nil>}
2026-03-25T12:05:27.4856294Z --network and --net in the options will be ignored.
2026-03-25T12:05:27.4856928Z Merged container.HostConfig ==> &{Binds:[/var/run/docker.sock:/var/run/docker.sock] ContainerIDFile: LogConfig:{Type: Config:map[]} NetworkMode:gitea_gitea_default PortBindings:map[] RestartPolicy:{Name:no MaximumRetryCount:0} AutoRemove:true VolumeDriver: VolumesFrom:[] ConsoleSize:[0 0] Annotations:map[] CapAdd:[] CapDrop:[] CgroupnsMode: DNS:[] DNSOptions:[] DNSSearch:[] ExtraHosts:[] GroupAdd:[] IpcMode: Cgroup: Links:[] OomScoreAdj:0 PidMode: Privileged:false PublishAllPorts:false ReadonlyRootfs:false SecurityOpt:[] StorageOpt:map[] Tmpfs:map[] UTSMode: UsernsMode: ShmSize:0 Sysctls:map[] Runtime: Isolation: Resources:{CPUShares:0 Memory:0 NanoCPUs:0 CgroupParent: BlkioWeight:0 BlkioWeightDevice:[] BlkioDeviceReadBps:[] BlkioDeviceWriteBps:[] BlkioDeviceReadIOps:[] BlkioDeviceWriteIOps:[] CPUPeriod:0 CPUQuota:0 CPURealtimePeriod:0 CPURealtimeRuntime:0 CpusetCpus: CpusetMems: Devices:[] DeviceCgroupRules:[] DeviceRequests:[] KernelMemory:0 KernelMemoryTCP:0 MemoryReservation:0 MemorySwap:0 MemorySwappiness:0x228ace413ac8 OomKillDisable:0x228ace4139c3 PidsLimit:0x228ace413b28 Ulimits:[] CPUCount:0 CPUPercent:0 IOMaximumIOps:0 IOMaximumBandwidth:0} Mounts:[{Type:volume Source:act-toolcache Target:/opt/hostedtoolcache ReadOnly:false Consistency: BindOptions:<nil> VolumeOptions:<nil> TmpfsOptions:<nil> ClusterOptions:<nil>} {Type:volume Source:GITEA-ACTIONS-TASK-1104_WORKFLOW-CI_JOB-build-env Target:/var/run/act ReadOnly:false Consistency: BindOptions:<nil> VolumeOptions:<nil> TmpfsOptions:<nil> ClusterOptions:<nil>} {Type:volume Source:GITEA-ACTIONS-TASK-1104_WORKFLOW-CI_JOB-build Target:/workspace/***/***3-server ReadOnly:false Consistency: BindOptions:<nil> VolumeOptions:<nil> TmpfsOptions:<nil> ClusterOptions:<nil>}] MaskedPaths:[] ReadonlyPaths:[] Init:<nil>}
2026-03-25T12:05:27.5978022Z Created container name=GITEA-ACTIONS-TASK-1104_WORKFLOW-CI_JOB-build id=7218bfea7789106d54e48224d5541c49969b5cca00766ae2ffdcbc415f9c35ae from image maven:3.9-eclipse-temurin-17 (platform: )
2026-03-25T12:05:27.5978558Z ENV ==> [RUNNER_TOOL_CACHE=/opt/hostedtoolcache RUNNER_OS=Linux RUNNER_ARCH=ARM64 RUNNER_TEMP=/tmp LANG=C.UTF-8]
2026-03-25T12:05:27.5978778Z 🐳 docker run image=maven:3.9-eclipse-temurin-17 platform= entrypoint=["/bin/sleep" "10800"] cmd=[] network="gitea_gitea_default"
2026-03-25T12:05:27.5978979Z Starting container: 7218bfea7789106d54e48224d5541c49969b5cca00766ae2ffdcbc415f9c35ae
2026-03-25T12:05:27.8074630Z Started container: 7218bfea7789106d54e48224d5541c49969b5cca00766ae2ffdcbc415f9c35ae
2026-03-25T12:05:27.9290310Z Writing entry to tarball workflow/event.json len:5028
2026-03-25T12:05:27.9290900Z Writing entry to tarball workflow/envs.txt len:0
2026-03-25T12:05:27.9291139Z Extracting content to '/var/run/act/'
2026-03-25T12:05:27.9477718Z ☁ git clone 'https://github.com/actions/checkout' # ref=v4
2026-03-25T12:05:27.9478182Z cloning https://github.com/actions/checkout to /root/.cache/act/c3fe249fe73091a17d6638fe1341e7bd0bcc3466ce52323c0688e83e2463a4ab
2026-03-25T12:05:28.2648306Z Non-terminating error while running 'git clone': some refs were not updated
2026-03-25T12:05:28.2717948Z ☁ git clone 'https://github.com/actions/cache' # ref=v4
2026-03-25T12:05:28.2718527Z cloning https://github.com/actions/cache to /root/.cache/act/6b4e4eb40e21c1bd02cb00a273f4d79af7c42205c1390e4e65c594ecd7a3696e
2026-03-25T12:05:29.0246163Z Non-terminating error while running 'git clone': some refs were not updated
2026-03-25T12:05:29.0434354Z evaluating expression ''
2026-03-25T12:05:29.0435137Z expression '' evaluated to 'true'
2026-03-25T12:05:29.0435325Z ⭐ Run Main Install Node.js 22
2026-03-25T12:05:29.0435581Z Writing entry to tarball workflow/outputcmd.txt len:0
2026-03-25T12:05:29.0435798Z Writing entry to tarball workflow/statecmd.txt len:0
2026-03-25T12:05:29.0435957Z Writing entry to tarball workflow/pathcmd.txt len:0
2026-03-25T12:05:29.0436104Z Writing entry to tarball workflow/envs.txt len:0
2026-03-25T12:05:29.0436243Z Writing entry to tarball workflow/SUMMARY.md len:0
2026-03-25T12:05:29.0436418Z Extracting content to '/var/run/act'
2026-03-25T12:05:29.0641886Z Wrote command \n\napt-get update && apt-get install -y ca-certificates curl gnupg\nmkdir -p /etc/apt/keyrings\ncurl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg\necho "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_22.x nodistro main" > /etc/apt/sources.list.d/nodesource.list\napt-get update && apt-get install -y nodejs\n\n\n to 'workflow/0.sh'
2026-03-25T12:05:29.0642653Z Writing entry to tarball workflow/0.sh len:407
2026-03-25T12:05:29.0642896Z Extracting content to '/var/run/act'
2026-03-25T12:05:29.0664683Z 🐳 docker exec cmd=[sh -e /var/run/act/workflow/0.sh] user= workdir=
2026-03-25T12:05:29.0665120Z Exec command '[sh -e /var/run/act/workflow/0.sh]'
2026-03-25T12:05:29.0665572Z Working directory '/workspace/***/***3-server'
2026-03-25T12:05:29.1930283Z Get:1 http://ports.ubuntu.com/ubuntu-ports noble InRelease [256 kB]
2026-03-25T12:05:29.3020283Z Get:2 http://ports.ubuntu.com/ubuntu-ports noble-updates InRelease [126 kB]
2026-03-25T12:05:29.3278285Z Get:3 http://ports.ubuntu.com/ubuntu-ports noble-backports InRelease [126 kB]
2026-03-25T12:05:29.3533160Z Get:4 http://ports.ubuntu.com/ubuntu-ports noble-security InRelease [126 kB]
2026-03-25T12:05:29.3799993Z Get:5 http://ports.ubuntu.com/ubuntu-ports noble/universe arm64 Packages [19.0 MB]
2026-03-25T12:05:29.8392869Z Get:6 http://ports.ubuntu.com/ubuntu-ports noble/restricted arm64 Packages [113 kB]
2026-03-25T12:05:29.8393771Z Get:7 http://ports.ubuntu.com/ubuntu-ports noble/multiverse arm64 Packages [274 kB]
2026-03-25T12:05:29.8394197Z Get:8 http://ports.ubuntu.com/ubuntu-ports noble/main arm64 Packages [1,776 kB]
2026-03-25T12:05:29.8802923Z Get:9 http://ports.ubuntu.com/ubuntu-ports noble-updates/multiverse arm64 Packages [45.7 kB]
2026-03-25T12:05:29.8812220Z Get:10 http://ports.ubuntu.com/ubuntu-ports noble-updates/main arm64 Packages [2,451 kB]
2026-03-25T12:05:29.9387427Z Get:11 http://ports.ubuntu.com/ubuntu-ports noble-updates/universe arm64 Packages [2,057 kB]
2026-03-25T12:05:29.9942591Z Get:12 http://ports.ubuntu.com/ubuntu-ports noble-updates/restricted arm64 Packages [5,019 kB]
2026-03-25T12:05:30.1164288Z Get:13 http://ports.ubuntu.com/ubuntu-ports noble-backports/multiverse arm64 Packages [695 B]
2026-03-25T12:05:30.1180686Z Get:14 http://ports.ubuntu.com/ubuntu-ports noble-backports/universe arm64 Packages [36.1 kB]
2026-03-25T12:05:30.1181823Z Get:15 http://ports.ubuntu.com/ubuntu-ports noble-backports/main arm64 Packages [49.5 kB]
2026-03-25T12:05:30.1182817Z Get:16 http://ports.ubuntu.com/ubuntu-ports noble-security/universe arm64 Packages [1,502 kB]
2026-03-25T12:05:30.1538385Z Get:17 http://ports.ubuntu.com/ubuntu-ports noble-security/multiverse arm64 Packages [44.1 kB]
2026-03-25T12:05:30.1539003Z Get:18 http://ports.ubuntu.com/ubuntu-ports noble-security/restricted arm64 Packages [4,828 kB]
2026-03-25T12:05:30.2657962Z Get:19 http://ports.ubuntu.com/ubuntu-ports noble-security/main arm64 Packages [2,087 kB]
2026-03-25T12:05:31.5132214Z Fetched 39.9 MB in 2s (16.9 MB/s)
2026-03-25T12:05:32.8027349Z Reading package lists...
2026-03-25T12:05:34.1413764Z Reading package lists...
2026-03-25T12:05:34.4862647Z Building dependency tree...
2026-03-25T12:05:34.4864531Z Reading state information...
2026-03-25T12:05:35.2536110Z ca-certificates is already the newest version (20240203).
2026-03-25T12:05:35.2536691Z curl is already the newest version (8.5.0-2ubuntu10.8).
2026-03-25T12:05:35.2536858Z gnupg is already the newest version (2.4.4-2ubuntu17.4).
2026-03-25T12:05:35.2537061Z 0 upgraded, 0 newly installed, 0 to remove and 18 not upgraded.
2026-03-25T12:05:35.5548236Z Get:1 https://deb.nodesource.com/node_22.x nodistro InRelease [12.1 kB]
2026-03-25T12:05:35.7037091Z Hit:2 http://ports.ubuntu.com/ubuntu-ports noble InRelease
2026-03-25T12:05:35.7322162Z Hit:3 http://ports.ubuntu.com/ubuntu-ports noble-updates InRelease
2026-03-25T12:05:35.7599694Z Hit:4 http://ports.ubuntu.com/ubuntu-ports noble-backports InRelease
2026-03-25T12:05:35.7870145Z Hit:5 http://ports.ubuntu.com/ubuntu-ports noble-security InRelease
2026-03-25T12:05:35.8551499Z Get:6 https://deb.nodesource.com/node_22.x nodistro/main arm64 Packages [9,040 B]
2026-03-25T12:05:35.9067803Z Fetched 21.2 kB in 1s (41.2 kB/s)
2026-03-25T12:05:37.2373298Z Reading package lists...
2026-03-25T12:05:38.5735995Z Reading package lists...
2026-03-25T12:05:38.9087319Z Building dependency tree...
2026-03-25T12:05:38.9087899Z Reading state information...
2026-03-25T12:05:39.5855767Z The following additional packages will be installed:
2026-03-25T12:05:39.5870559Z libpython3-stdlib libpython3.12-minimal libpython3.12-stdlib media-types
2026-03-25T12:05:39.5880369Z netbase python3 python3-minimal python3.12 python3.12-minimal
2026-03-25T12:05:39.5913231Z Suggested packages:
2026-03-25T12:05:39.5913883Z python3-doc python3-tk python3-venv python3.12-venv python3.12-doc
2026-03-25T12:05:39.5914038Z binfmt-support
2026-03-25T12:05:39.6741455Z The following NEW packages will be installed:
2026-03-25T12:05:39.6754401Z libpython3-stdlib libpython3.12-minimal libpython3.12-stdlib media-types
2026-03-25T12:05:39.6762410Z netbase nodejs python3 python3-minimal python3.12 python3.12-minimal
2026-03-25T12:05:39.7694458Z 0 upgraded, 10 newly installed, 0 to remove and 18 not upgraded.
2026-03-25T12:05:39.7694946Z Need to get 42.9 MB of archives.
2026-03-25T12:05:39.7695098Z After this operation, 259 MB of additional disk space will be used.
2026-03-25T12:05:39.7695287Z Get:1 http://ports.ubuntu.com/ubuntu-ports noble-updates/main arm64 libpython3.12-minimal arm64 3.12.3-1ubuntu0.12 [834 kB]
2026-03-25T12:05:39.8854035Z Get:2 https://deb.nodesource.com/node_22.x nodistro/main arm64 nodejs arm64 22.22.1-1nodesource1 [37.0 MB]
2026-03-25T12:05:39.9473201Z Get:3 http://ports.ubuntu.com/ubuntu-ports noble-updates/main arm64 python3.12-minimal arm64 3.12.3-1ubuntu0.12 [2,252 kB]
2026-03-25T12:05:40.0068444Z Get:4 http://ports.ubuntu.com/ubuntu-ports noble-updates/main arm64 python3-minimal arm64 3.12.3-0ubuntu2.1 [27.4 kB]
2026-03-25T12:05:40.0073156Z Get:5 http://ports.ubuntu.com/ubuntu-ports noble/main arm64 media-types all 10.1.0 [27.5 kB]
2026-03-25T12:05:40.0077139Z Get:6 http://ports.ubuntu.com/ubuntu-ports noble/main arm64 netbase all 6.4 [13.1 kB]
2026-03-25T12:05:40.0079975Z Get:7 http://ports.ubuntu.com/ubuntu-ports noble-updates/main arm64 libpython3.12-stdlib arm64 3.12.3-1ubuntu0.12 [2,037 kB]
2026-03-25T12:05:40.0421355Z Get:8 http://ports.ubuntu.com/ubuntu-ports noble-updates/main arm64 python3.12 arm64 3.12.3-1ubuntu0.12 [651 kB]
2026-03-25T12:05:40.0818905Z Get:9 http://ports.ubuntu.com/ubuntu-ports noble-updates/main arm64 libpython3-stdlib arm64 3.12.3-0ubuntu2.1 [10.1 kB]
2026-03-25T12:05:40.0822085Z Get:10 http://ports.ubuntu.com/ubuntu-ports noble-updates/main arm64 python3 arm64 3.12.3-0ubuntu2.1 [23.0 kB]
2026-03-25T12:05:41.0927970Z debconf: delaying package configuration, since apt-utils is not installed
2026-03-25T12:05:41.1546756Z Fetched 42.9 MB in 1s (38.8 MB/s)
2026-03-25T12:05:41.2105330Z Selecting previously unselected package libpython3.12-minimal:arm64.
2026-03-25T12:05:41.2141434Z (Reading database ...
(Reading database ... 5%
(Reading database ... 10%
(Reading database ... 15%
(Reading database ... 20%
(Reading database ... 25%
(Reading database ... 30%
(Reading database ... 35%
(Reading database ... 40%
(Reading database ... 45%
(Reading database ... 50%
(Reading database ... 55%
(Reading database ... 60%
(Reading database ... 65%
(Reading database ... 70%
(Reading database ... 75%
(Reading database ... 80%
(Reading database ... 85%
(Reading database ... 90%
(Reading database ... 95%
(Reading database ... 100%
(Reading database ... 10203 files and directories currently installed.)
2026-03-25T12:05:41.2151206Z Preparing to unpack .../libpython3.12-minimal_3.12.3-1ubuntu0.12_arm64.deb ...
2026-03-25T12:05:41.2237074Z Unpacking libpython3.12-minimal:arm64 (3.12.3-1ubuntu0.12) ...
2026-03-25T12:05:41.3575893Z Selecting previously unselected package python3.12-minimal.
2026-03-25T12:05:41.3590278Z Preparing to unpack .../python3.12-minimal_3.12.3-1ubuntu0.12_arm64.deb ...
2026-03-25T12:05:41.3695167Z Unpacking python3.12-minimal (3.12.3-1ubuntu0.12) ...
2026-03-25T12:05:41.4592573Z Setting up libpython3.12-minimal:arm64 (3.12.3-1ubuntu0.12) ...
2026-03-25T12:05:41.4945507Z Setting up python3.12-minimal (3.12.3-1ubuntu0.12) ...
2026-03-25T12:05:42.5868849Z Selecting previously unselected package python3-minimal.
2026-03-25T12:05:42.5916327Z (Reading database ...
(Reading database ... 5%
(Reading database ... 10%
(Reading database ... 15%
(Reading database ... 20%
(Reading database ... 25%
(Reading database ... 30%
(Reading database ... 35%
(Reading database ... 40%
(Reading database ... 45%
(Reading database ... 50%
(Reading database ... 55%
(Reading database ... 60%
(Reading database ... 65%
(Reading database ... 70%
(Reading database ... 75%
(Reading database ... 80%
(Reading database ... 85%
(Reading database ... 90%
(Reading database ... 95%
(Reading database ... 100%
(Reading database ... 10514 files and directories currently installed.)
2026-03-25T12:05:42.5927206Z Preparing to unpack .../0-python3-minimal_3.12.3-0ubuntu2.1_arm64.deb ...
2026-03-25T12:05:42.6014142Z Unpacking python3-minimal (3.12.3-0ubuntu2.1) ...
2026-03-25T12:05:42.6550947Z Selecting previously unselected package media-types.
2026-03-25T12:05:42.6565762Z Preparing to unpack .../1-media-types_10.1.0_all.deb ...
2026-03-25T12:05:42.6650215Z Unpacking media-types (10.1.0) ...
2026-03-25T12:05:42.7298448Z Selecting previously unselected package netbase.
2026-03-25T12:05:42.7308345Z Preparing to unpack .../2-netbase_6.4_all.deb ...
2026-03-25T12:05:42.7390454Z Unpacking netbase (6.4) ...
2026-03-25T12:05:42.7929628Z Selecting previously unselected package libpython3.12-stdlib:arm64.
2026-03-25T12:05:42.7930194Z Preparing to unpack .../3-libpython3.12-stdlib_3.12.3-1ubuntu0.12_arm64.deb ...
2026-03-25T12:05:42.8009455Z Unpacking libpython3.12-stdlib:arm64 (3.12.3-1ubuntu0.12) ...
2026-03-25T12:05:42.9412131Z Selecting previously unselected package python3.12.
2026-03-25T12:05:42.9416942Z Preparing to unpack .../4-python3.12_3.12.3-1ubuntu0.12_arm64.deb ...
2026-03-25T12:05:42.9542497Z Unpacking python3.12 (3.12.3-1ubuntu0.12) ...
2026-03-25T12:05:43.0104645Z Selecting previously unselected package libpython3-stdlib:arm64.
2026-03-25T12:05:43.0105209Z Preparing to unpack .../5-libpython3-stdlib_3.12.3-0ubuntu2.1_arm64.deb ...
2026-03-25T12:05:43.0184920Z Unpacking libpython3-stdlib:arm64 (3.12.3-0ubuntu2.1) ...
2026-03-25T12:05:43.0704590Z Setting up python3-minimal (3.12.3-0ubuntu2.1) ...
2026-03-25T12:05:43.2911706Z Selecting previously unselected package python3.
2026-03-25T12:05:43.2943227Z (Reading database ...
(Reading database ... 5%
(Reading database ... 10%
(Reading database ... 15%
(Reading database ... 20%
(Reading database ... 25%
(Reading database ... 30%
(Reading database ... 35%
(Reading database ... 40%
(Reading database ... 45%
(Reading database ... 50%
(Reading database ... 55%
(Reading database ... 60%
(Reading database ... 65%
(Reading database ... 70%
(Reading database ... 75%
(Reading database ... 80%
(Reading database ... 85%
(Reading database ... 90%
(Reading database ... 95%
(Reading database ... 100%
(Reading database ... 10955 files and directories currently installed.)
2026-03-25T12:05:43.2945936Z Preparing to unpack .../python3_3.12.3-0ubuntu2.1_arm64.deb ...
2026-03-25T12:05:43.3054683Z Unpacking python3 (3.12.3-0ubuntu2.1) ...
2026-03-25T12:05:43.3791632Z Selecting previously unselected package nodejs.
2026-03-25T12:05:43.3792145Z Preparing to unpack .../nodejs_22.22.1-1nodesource1_arm64.deb ...
2026-03-25T12:05:43.3883396Z Unpacking nodejs (22.22.1-1nodesource1) ...
2026-03-25T12:05:45.1595580Z Setting up media-types (10.1.0) ...
2026-03-25T12:05:45.1928178Z Setting up netbase (6.4) ...
2026-03-25T12:05:45.2516737Z Setting up libpython3.12-stdlib:arm64 (3.12.3-1ubuntu0.12) ...
2026-03-25T12:05:45.2763302Z Setting up python3.12 (3.12.3-1ubuntu0.12) ...
2026-03-25T12:05:46.4572507Z Setting up libpython3-stdlib:arm64 (3.12.3-0ubuntu2.1) ...
2026-03-25T12:05:46.4827528Z Setting up python3 (3.12.3-0ubuntu2.1) ...
2026-03-25T12:05:46.5029071Z running python rtupdate hooks for python3.12...
2026-03-25T12:05:46.5029813Z running python post-rtupdate hooks for python3.12...
2026-03-25T12:05:46.6148316Z Setting up nodejs (22.22.1-1nodesource1) ...
2026-03-25T12:05:47.1121917Z ::add-matcher::/run/act/actions/c3fe249fe73091a17d6638fe1341e7bd0bcc3466ce52323c0688e83e2463a4ab/dist/problem-matcher.json
2026-03-25T12:05:47.1125470Z Syncing repository: ***/***3-server
2026-03-25T12:05:47.1131969Z ::group::Getting Git version info
2026-03-25T12:05:47.1168484Z Working directory is '/workspace/***/***3-server'
2026-03-25T12:05:47.1181789Z [command]/usr/bin/git version
2026-03-25T12:05:47.1229566Z git version 2.43.0
2026-03-25T12:05:47.1261791Z ::endgroup::
2026-03-25T12:05:47.1293796Z Temporarily overriding HOME='/tmp/5934957e-e423-4d52-bf2d-bec37cc99fdb' before making global git config changes
2026-03-25T12:05:47.1295018Z Adding repository directory to the temporary git global config as a safe directory
2026-03-25T12:05:47.1305207Z [command]/usr/bin/git config --global --add safe.directory /workspace/***/***3-server
2026-03-25T12:05:47.1341779Z Deleting the contents of '/workspace/***/***3-server'
2026-03-25T12:05:47.1347568Z ::group::Initializing the repository
2026-03-25T12:05:47.1353328Z [command]/usr/bin/git init /workspace/***/***3-server
2026-03-25T12:05:47.1387743Z hint: Using 'master' as the name for the initial branch. This default branch name
2026-03-25T12:05:47.1388199Z hint: is subject to change. To configure the initial branch name to use in all
2026-03-25T12:05:47.1388360Z hint: of your new repositories, which will suppress this warning, call:
2026-03-25T12:05:47.1388505Z hint:
2026-03-25T12:05:47.1388717Z hint: git config --global init.defaultBranch <name>
2026-03-25T12:05:47.1388847Z hint:
2026-03-25T12:05:47.1389121Z hint: Names commonly chosen instead of 'master' are 'main', 'trunk' and
2026-03-25T12:05:47.1389269Z hint: 'development'. The just-created branch can be renamed via this command:
2026-03-25T12:05:47.1389422Z hint:
2026-03-25T12:05:47.1389553Z hint: git branch -m <name>
2026-03-25T12:05:47.1394082Z Initialized empty Git repository in /workspace/***/***3-server/.git/
2026-03-25T12:05:47.1412922Z [command]/usr/bin/git remote add origin https://gitea.siegeln.net/***/***3-server
2026-03-25T12:05:47.1454222Z ::endgroup::
2026-03-25T12:05:47.1455198Z ::group::Disabling automatic garbage collection
2026-03-25T12:05:47.1459932Z [command]/usr/bin/git config --local gc.auto 0
2026-03-25T12:05:47.1511236Z ::endgroup::
2026-03-25T12:05:47.1511645Z ::group::Setting up auth
2026-03-25T12:05:47.1516443Z [command]/usr/bin/git config --local --name-only --get-regexp core\.sshCommand
2026-03-25T12:05:47.1557420Z [command]/usr/bin/git submodule foreach --recursive sh -c "git config --local --name-only --get-regexp 'core\.sshCommand' && git config --local --unset-all 'core.sshCommand' || :"
2026-03-25T12:05:47.1744851Z [command]/usr/bin/git config --local --name-only --get-regexp http\.https\:\/\/gitea\.siegeln\.net\/\.extraheader
2026-03-25T12:05:47.1778167Z [command]/usr/bin/git submodule foreach --recursive sh -c "git config --local --name-only --get-regexp 'http\.https\:\/\/gitea\.siegeln\.net\/\.extraheader' && git config --local --unset-all 'http.https://gitea.siegeln.net/.extraheader' || :"
2026-03-25T12:05:47.1961322Z [command]/usr/bin/git config --local --name-only --get-regexp ^includeIf\.gitdir:
2026-03-25T12:05:47.1992646Z [command]/usr/bin/git submodule foreach --recursive git config --local --show-origin --name-only --get-regexp remote.origin.url
2026-03-25T12:05:47.2169231Z [command]/usr/bin/git config --local http.https://gitea.siegeln.net/.extraheader AUTHORIZATION: basic ***
2026-03-25T12:05:47.2203274Z ::endgroup::
2026-03-25T12:05:47.2203661Z ::group::Fetching the repository
2026-03-25T12:05:47.2227031Z [command]/usr/bin/git -c protocol.version=2 fetch --no-tags --prune --no-recurse-submodules --depth=1 origin +c96fbef5d517a255fbebb231cfe07c32dd7af9a2:refs/remotes/origin/main
2026-03-25T12:05:47.5225105Z From https://gitea.siegeln.net/***/***3-server
2026-03-25T12:05:47.5225789Z * [new ref] c96fbef5d517a255fbebb231cfe07c32dd7af9a2 -> origin/main
2026-03-25T12:05:47.5242588Z ::endgroup::
2026-03-25T12:05:47.5242939Z ::group::Determining the checkout info
2026-03-25T12:05:47.5245115Z ::endgroup::
2026-03-25T12:05:47.5250211Z [command]/usr/bin/git sparse-checkout disable
2026-03-25T12:05:47.5286780Z [command]/usr/bin/git config --local --unset-all extensions.worktreeConfig
2026-03-25T12:05:47.5312590Z ::group::Checking out the ref
2026-03-25T12:05:47.5317724Z [command]/usr/bin/git checkout --progress --force -B main refs/remotes/origin/main
2026-03-25T12:05:47.5620122Z Switched to a new branch 'main'
2026-03-25T12:05:47.5620634Z branch 'main' set up to track 'origin/main'.
2026-03-25T12:05:47.5626584Z ::endgroup::
2026-03-25T12:05:47.5669948Z [command]/usr/bin/git log -1 --format=%H
2026-03-25T12:05:47.5692040Z c96fbef5d517a255fbebb231cfe07c32dd7af9a2
2026-03-25T12:05:47.5706708Z ::remove-matcher owner=checkout-git::
2026-03-25T12:05:48.3567196Z (node:674) [DEP0040] DeprecationWarning: The `punycode` module is deprecated. Please use a userland alternative instead.
2026-03-25T12:05:48.3567623Z (Use `node --trace-deprecation ...` to show where the warning was created)
2026-03-25T12:05:49.1970440Z Cache Size: ~352 MB (368583412 B)
2026-03-25T12:05:49.2003055Z [command]/usr/bin/tar -xf /tmp/45f912d1-a147-43aa-9691-70814448aaf7/cache.tgz -P -C /workspace/***/***3-server -z
2026-03-25T12:05:52.7483801Z Cache restored successfully
2026-03-25T12:05:52.7951677Z Cache restored from key: linux-maven-9dd3df74bd8aba32251bb49c59e820a2aa73582b0f09ca6d6e930542cfb05391
2026-03-25T12:05:59.8118885Z
2026-03-25T12:05:59.8119616Z added 213 packages, and audited 214 packages in 7s
2026-03-25T12:05:59.8120801Z
2026-03-25T12:05:59.8121169Z 58 packages are looking for funding
2026-03-25T12:05:59.8121449Z run `npm fund` for details
2026-03-25T12:05:59.8132812Z
2026-03-25T12:05:59.8133050Z found 0 vulnerabilities
2026-03-25T12:05:59.9967402Z
2026-03-25T12:05:59.9967960Z > ui@0.0.0 build
2026-03-25T12:05:59.9968142Z > tsc -p tsconfig.app.json --noEmit && vite build
2026-03-25T12:05:59.9968297Z
2026-03-25T12:06:06.5319156Z vite v8.0.1 building client environment for production...
2026-03-25T12:06:07.1364525Z 
transforming...✓ 129 modules transformed.
2026-03-25T12:06:07.2755293Z rendering chunks...
2026-03-25T12:06:07.6308430Z computing gzip size...
2026-03-25T12:06:07.6646343Z dist/index.html 0.94 kB │ gzip: 0.41 kB
2026-03-25T12:06:07.6646953Z dist/assets/dm-sans-600-Aqo67rzb.woff2 14.14 kB
2026-03-25T12:06:07.6647195Z dist/assets/dm-sans-400-CW0RaeGs.woff2 14.20 kB
2026-03-25T12:06:07.6647355Z dist/assets/dm-sans-500-B9HHJjqV.woff2 14.30 kB
2026-03-25T12:06:07.6647500Z dist/assets/dm-sans-700-DvUfVpUG.woff2 14.34 kB
2026-03-25T12:06:07.6647639Z dist/assets/dm-sans-400-italic-DRLHr0TN.woff2 15.14 kB
2026-03-25T12:06:07.6647798Z dist/assets/jetbrains-mono-400-V6pRDFza.woff2 21.16 kB
2026-03-25T12:06:07.6647949Z dist/assets/jetbrains-mono-500-BWZEU5yA.woff2 21.83 kB
2026-03-25T12:06:07.6648100Z dist/assets/jetbrains-mono-600-C8RAYTDA.woff2 21.86 kB
2026-03-25T12:06:07.6648237Z dist/assets/OidcConfigPage-Bq9_k9q2.css 0.61 kB │ gzip: 0.31 kB
2026-03-25T12:06:07.6648385Z dist/assets/AuditLogPage-wcUcdzwn.css 1.19 kB │ gzip: 0.56 kB
2026-03-25T12:06:07.6648541Z dist/assets/RoutesMetrics-DaUBduav.css 1.46 kB │ gzip: 0.62 kB
2026-03-25T12:06:07.6648704Z dist/assets/RbacPage-5iGc4gch.css 1.98 kB │ gzip: 0.69 kB
2026-03-25T12:06:07.6648857Z dist/assets/AgentInstance--Z0IKMF0.css 2.50 kB │ gzip: 0.75 kB
2026-03-25T12:06:07.6649011Z dist/assets/AgentHealth-y0_55tmK.css 3.42 kB │ gzip: 1.04 kB
2026-03-25T12:06:07.6649159Z dist/assets/Dashboard-BYYJXEtx.css 3.64 kB │ gzip: 1.13 kB
2026-03-25T12:06:07.6649404Z dist/assets/RouteDetail-CjL6IMio.css 4.26 kB │ gzip: 1.09 kB
2026-03-25T12:06:07.6649547Z dist/assets/ExchangeDetail-VcItziWb.css 6.78 kB │ gzip: 1.67 kB
2026-03-25T12:06:07.6649698Z dist/assets/index-CXegvd7B.css 111.14 kB │ gzip: 18.21 kB
2026-03-25T12:06:07.6649862Z dist/assets/SwaggerPage-CH1CJXTy.css 176.70 kB │ gzip: 26.18 kB
2026-03-25T12:06:07.6650017Z dist/assets/use-refresh-interval-CBD44ngw.js 0.19 kB │ gzip: 0.15 kB
2026-03-25T12:06:07.6650157Z dist/assets/admin-api-Dldh4fYm.js 0.49 kB │ gzip: 0.35 kB
2026-03-25T12:06:07.6650297Z dist/assets/AdminLayout-CKg4TLhS.js 0.55 kB │ gzip: 0.34 kB
2026-03-25T12:06:07.6650468Z dist/assets/chunk-B3K2TuZy.js 0.55 kB │ gzip: 0.35 kB
2026-03-25T12:06:07.6650606Z dist/assets/agent-metrics-DI4HNJNm.js 0.59 kB │ gzip: 0.42 kB
2026-03-25T12:06:07.6650776Z dist/assets/SwaggerPage-ZwDCrEj1.js 0.87 kB │ gzip: 0.54 kB
2026-03-25T12:06:07.6650931Z dist/assets/diagram-mapping-zsjZZKRP.js 1.33 kB │ gzip: 0.68 kB
2026-03-25T12:06:07.6651084Z dist/assets/useMutation-CjGnzIYg.js 2.31 kB │ gzip: 0.97 kB
2026-03-25T12:06:07.6651227Z dist/assets/OpenSearchAdminPage-WnM4gikq.js 2.92 kB │ gzip: 1.17 kB
2026-03-25T12:06:07.6651367Z dist/assets/DatabaseAdminPage-CclQJ0Ok.js 3.09 kB │ gzip: 1.16 kB
2026-03-25T12:06:07.6651574Z dist/assets/AuditLogPage-4V3oK99a.js 4.53 kB │ gzip: 1.70 kB
2026-03-25T12:06:07.6651718Z dist/assets/OidcConfigPage-D0YsPv3U.js 5.38 kB │ gzip: 1.90 kB
2026-03-25T12:06:07.6651866Z dist/assets/RoutesMetrics-5XMqf33P.js 5.98 kB │ gzip: 2.10 kB
2026-03-25T12:06:07.6652008Z dist/assets/jsx-runtime-DdsoV0Vr.js 9.07 kB │ gzip: 3.52 kB
2026-03-25T12:06:07.6652178Z dist/assets/AgentInstance-Bzs6lRBr.js 9.86 kB │ gzip: 2.83 kB
2026-03-25T12:06:07.6652328Z dist/assets/Dashboard-C23kCbUs.js 10.16 kB │ gzip: 3.28 kB
2026-03-25T12:06:07.6652472Z dist/assets/auth-store-DCBo9Ans.js 10.34 kB │ gzip: 3.79 kB
2026-03-25T12:06:07.6652628Z dist/assets/AgentHealth-DwOu1dM9.js 11.36 kB │ gzip: 3.28 kB
2026-03-25T12:06:07.6652774Z dist/assets/RouteDetail-l02TOYKF.js 12.35 kB │ gzip: 4.07 kB
2026-03-25T12:06:07.6652926Z dist/assets/ExchangeDetail-BLpXn9YT.js 14.56 kB │ gzip: 4.25 kB
2026-03-25T12:06:07.6653071Z dist/assets/useQuery-oR6RVwVH.js 22.31 kB │ gzip: 7.37 kB
2026-03-25T12:06:07.6653215Z dist/assets/RbacPage-oz3jLDxG.js 25.49 kB │ gzip: 5.57 kB
2026-03-25T12:06:07.6653360Z dist/assets/index-COBtXOCg.js 205.44 kB │ gzip: 64.73 kB
2026-03-25T12:06:07.6653503Z dist/assets/index.es-BideRJQn.js 216.16 kB │ gzip: 66.22 kB
2026-03-25T12:06:07.6653661Z dist/assets/swagger-ui-bundle-BoDRmAdn.js 1,371.99 kB │ gzip: 390.77 kB
2026-03-25T12:06:07.6653794Z
2026-03-25T12:06:07.6657850Z ✓ built in 1.13s
2026-03-25T12:06:07.6667869Z [plugin builtin:vite-reporter]
2026-03-25T12:06:07.6668214Z (!) Some chunks are larger than 500 kB after minification. Consider:
2026-03-25T12:06:07.6668402Z - Using dynamic import() to code-split the application
2026-03-25T12:06:07.6668544Z - Use build.rolldownOptions.output.codeSplitting to improve chunking: https://rolldown.rs/reference/OutputOptions.codeSplitting
2026-03-25T12:06:07.6668727Z - Adjust chunk size limit for this warning via build.chunkSizeWarningLimit.
2026-03-25T12:06:09.5292704Z [INFO] Scanning for projects...
2026-03-25T12:06:09.9635324Z [INFO] ------------------------------------------------------------------------
2026-03-25T12:06:09.9636148Z [INFO] Reactor Build Order:
2026-03-25T12:06:09.9636467Z [INFO]
2026-03-25T12:06:09.9659474Z [INFO] Cameleer3 Server Parent [pom]
2026-03-25T12:06:09.9660313Z [INFO] Cameleer3 Server Core [jar]
2026-03-25T12:06:09.9660795Z [INFO] Cameleer3 Server App [jar]
2026-03-25T12:06:09.9828555Z [INFO]
2026-03-25T12:06:09.9829990Z [INFO] ---------------< com.***3:***3-server-parent >----------------
2026-03-25T12:06:09.9830445Z [INFO] Building Cameleer3 Server Parent 1.0-SNAPSHOT [1/3]
2026-03-25T12:06:09.9831993Z [INFO] from pom.xml
2026-03-25T12:06:09.9832701Z [INFO] --------------------------------[ pom ]---------------------------------
2026-03-25T12:06:10.0308726Z [INFO]
2026-03-25T12:06:10.0310168Z [INFO] --- clean:3.4.1:clean (default-clean) @ ***3-server-parent ---
2026-03-25T12:06:10.1462826Z [INFO]
2026-03-25T12:06:10.1463418Z [INFO] ----------------< com.***3:***3-server-core >-----------------
2026-03-25T12:06:10.1463669Z [INFO] Building Cameleer3 Server Core 1.0-SNAPSHOT [2/3]
2026-03-25T12:06:10.1464039Z [INFO] from ***3-server-core/pom.xml
2026-03-25T12:06:10.1464250Z [INFO] --------------------------------[ jar ]---------------------------------
2026-03-25T12:06:10.4658491Z [INFO]
2026-03-25T12:06:10.4659065Z [INFO] --- clean:3.4.1:clean (default-clean) @ ***3-server-core ---
2026-03-25T12:06:10.4666288Z [INFO]
2026-03-25T12:06:10.4667848Z [INFO] --- resources:3.3.1:resources (default-resources) @ ***3-server-core ---
2026-03-25T12:06:10.6497862Z [INFO] skip non existing resourceDirectory /workspace/***/***3-server/***3-server-core/src/main/resources
2026-03-25T12:06:10.6498571Z [INFO] skip non existing resourceDirectory /workspace/***/***3-server/***3-server-core/src/main/resources
2026-03-25T12:06:10.6506664Z [INFO]
2026-03-25T12:06:10.6508453Z [INFO] --- compiler:3.13.0:compile (default-compile) @ ***3-server-core ---
2026-03-25T12:06:10.8563867Z [INFO] Recompiling the module because of changed source code.
2026-03-25T12:06:10.8674155Z [INFO] Compiling 63 source files with javac [debug parameters release 17] to target/classes
2026-03-25T12:06:12.7604194Z [INFO] -------------------------------------------------------------
2026-03-25T12:06:12.7607101Z [ERROR] COMPILATION ERROR :
2026-03-25T12:06:12.7610106Z [INFO] -------------------------------------------------------------
2026-03-25T12:06:12.7612996Z [ERROR] /workspace/***/***3-server/***3-server-core/src/main/java/com/***3/server/core/logging/LogIndexService.java:[3,34] cannot find symbol
2026-03-25T12:06:12.7613413Z symbol: class LogEntry
2026-03-25T12:06:12.7614388Z location: package com.***3.common.model
2026-03-25T12:06:12.7616233Z [ERROR] /workspace/***/***3-server/***3-server-core/src/main/java/com/***3/server/core/logging/LogIndexService.java:[9,62] cannot find symbol
2026-03-25T12:06:12.7616604Z symbol: class LogEntry
2026-03-25T12:06:12.7617505Z location: interface com.***3.server.core.logging.LogIndexService
2026-03-25T12:06:12.7620033Z [INFO] 2 errors
2026-03-25T12:06:12.7622462Z [INFO] -------------------------------------------------------------
2026-03-25T12:06:12.7631912Z [INFO] ------------------------------------------------------------------------
2026-03-25T12:06:12.7640947Z [INFO] Reactor Summary for Cameleer3 Server Parent 1.0-SNAPSHOT:
2026-03-25T12:06:12.7641508Z [INFO]
2026-03-25T12:06:12.7641764Z [INFO] Cameleer3 Server Parent ............................ SUCCESS [ 0.164 s]
2026-03-25T12:06:12.7643553Z [INFO] Cameleer3 Server Core .............................. FAILURE [ 2.617 s]
2026-03-25T12:06:12.7646103Z [INFO] Cameleer3 Server App ............................... SKIPPED
2026-03-25T12:06:12.7647941Z [INFO] ------------------------------------------------------------------------
2026-03-25T12:06:12.7649783Z [INFO] BUILD FAILURE
2026-03-25T12:06:12.7651509Z [INFO] ------------------------------------------------------------------------
2026-03-25T12:06:12.7654688Z [INFO] Total time: 3.272 s
2026-03-25T12:06:12.7658348Z [INFO] Finished at: 2026-03-25T12:06:12Z
2026-03-25T12:06:12.7660448Z [INFO] ------------------------------------------------------------------------
2026-03-25T12:06:12.7673796Z [ERROR] Failed to execute goal org.apache.maven.plugins:maven-compiler-plugin:3.13.0:compile (default-compile) on project ***3-server-core: Compilation failure: Compilation failure:
2026-03-25T12:06:12.7676849Z [ERROR] /workspace/***/***3-server/***3-server-core/src/main/java/com/***3/server/core/logging/LogIndexService.java:[3,34] cannot find symbol
2026-03-25T12:06:12.7678889Z [ERROR] symbol: class LogEntry
2026-03-25T12:06:12.7680952Z [ERROR] location: package com.***3.common.model

488
ci-log2.txt Normal file
View File

@@ -0,0 +1,488 @@
2026-03-25T12:11:15.0673241Z my-docker-runner(version:v0.3.0) received task 1109 of job build, be triggered by event: push
2026-03-25T12:11:15.0678286Z workflow prepared
2026-03-25T12:11:15.0679524Z evaluating expression 'github.event_name != 'delete''
2026-03-25T12:11:15.0680588Z expression 'github.event_name != 'delete'' evaluated to 'true'
2026-03-25T12:11:15.0680862Z 🚀 Start image=maven:3.9-eclipse-temurin-17
2026-03-25T12:11:15.0765600Z 🐳 docker pull image=maven:3.9-eclipse-temurin-17 platform= username= forcePull=true
2026-03-25T12:11:15.0766370Z 🐳 docker pull maven:3.9-eclipse-temurin-17
2026-03-25T12:11:15.0766922Z pulling image 'docker.io/library/maven:3.9-eclipse-temurin-17' ()
2026-03-25T12:11:15.9297560Z Pulling from library/maven :: 3.9-eclipse-temurin-17
2026-03-25T12:11:15.9512271Z Digest: sha256:39a5260d49fe20e5f407bf63f63a267d9870965bcd1d114e52e1e50ba1c55a32 ::
2026-03-25T12:11:15.9512734Z Status: Image is up to date for maven:3.9-eclipse-temurin-17 ::
2026-03-25T12:11:15.9638746Z 🐳 docker create image=maven:3.9-eclipse-temurin-17 platform= entrypoint=["/bin/sleep" "10800"] cmd=[] network="gitea_gitea_default"
2026-03-25T12:11:15.9735982Z Custom container.Config from options ==> &{Hostname: Domainname: User: AttachStdin:false AttachStdout:true AttachStderr:true ExposedPorts:map[] Tty:false OpenStdin:false StdinOnce:false Env:[] Cmd:[] Healthcheck:<nil> ArgsEscaped:false Image: Volumes:map[] WorkingDir: Entrypoint:[] NetworkDisabled:false MacAddress: OnBuild:[] Labels:map[] StopSignal: StopTimeout:<nil> Shell:[]}
2026-03-25T12:11:15.9736700Z Merged container.Config ==> &{Hostname: Domainname: User: AttachStdin:false AttachStdout:true AttachStderr:true ExposedPorts:map[] Tty:false OpenStdin:false StdinOnce:false Env:[RUNNER_TOOL_CACHE=/opt/hostedtoolcache RUNNER_OS=Linux RUNNER_ARCH=ARM64 RUNNER_TEMP=/tmp LANG=C.UTF-8] Cmd:[] Healthcheck:<nil> ArgsEscaped:false Image:maven:3.9-eclipse-temurin-17 Volumes:map[] WorkingDir:/workspace/***/***3-server Entrypoint:[/bin/sleep 10800] NetworkDisabled:false MacAddress: OnBuild:[] Labels:map[] StopSignal: StopTimeout:<nil> Shell:[]}
2026-03-25T12:11:15.9737364Z Custom container.HostConfig from options ==> &{Binds:[] ContainerIDFile: LogConfig:{Type: Config:map[]} NetworkMode:gitea_gitea_default PortBindings:map[] RestartPolicy:{Name:no MaximumRetryCount:0} AutoRemove:false VolumeDriver: VolumesFrom:[] ConsoleSize:[0 0] Annotations:map[] CapAdd:[] CapDrop:[] CgroupnsMode: DNS:[] DNSOptions:[] DNSSearch:[] ExtraHosts:[] GroupAdd:[] IpcMode: Cgroup: Links:[] OomScoreAdj:0 PidMode: Privileged:false PublishAllPorts:false ReadonlyRootfs:false SecurityOpt:[] StorageOpt:map[] Tmpfs:map[] UTSMode: UsernsMode: ShmSize:0 Sysctls:map[] Runtime: Isolation: Resources:{CPUShares:0 Memory:0 NanoCPUs:0 CgroupParent: BlkioWeight:0 BlkioWeightDevice:[] BlkioDeviceReadBps:[] BlkioDeviceWriteBps:[] BlkioDeviceReadIOps:[] BlkioDeviceWriteIOps:[] CPUPeriod:0 CPUQuota:0 CPURealtimePeriod:0 CPURealtimeRuntime:0 CpusetCpus: CpusetMems: Devices:[] DeviceCgroupRules:[] DeviceRequests:[] KernelMemory:0 KernelMemoryTCP:0 MemoryReservation:0 MemorySwap:0 MemorySwappiness:0x228ace0e23c8 OomKillDisable:0x228ace0e22c3 PidsLimit:0x228ace0e2428 Ulimits:[] CPUCount:0 CPUPercent:0 IOMaximumIOps:0 IOMaximumBandwidth:0} Mounts:[] MaskedPaths:[] ReadonlyPaths:[] Init:<nil>}
2026-03-25T12:11:15.9738107Z --network and --net in the options will be ignored.
2026-03-25T12:11:15.9738529Z Merged container.HostConfig ==> &{Binds:[/var/run/docker.sock:/var/run/docker.sock] ContainerIDFile: LogConfig:{Type: Config:map[]} NetworkMode:gitea_gitea_default PortBindings:map[] RestartPolicy:{Name:no MaximumRetryCount:0} AutoRemove:true VolumeDriver: VolumesFrom:[] ConsoleSize:[0 0] Annotations:map[] CapAdd:[] CapDrop:[] CgroupnsMode: DNS:[] DNSOptions:[] DNSSearch:[] ExtraHosts:[] GroupAdd:[] IpcMode: Cgroup: Links:[] OomScoreAdj:0 PidMode: Privileged:false PublishAllPorts:false ReadonlyRootfs:false SecurityOpt:[] StorageOpt:map[] Tmpfs:map[] UTSMode: UsernsMode: ShmSize:0 Sysctls:map[] Runtime: Isolation: Resources:{CPUShares:0 Memory:0 NanoCPUs:0 CgroupParent: BlkioWeight:0 BlkioWeightDevice:[] BlkioDeviceReadBps:[] BlkioDeviceWriteBps:[] BlkioDeviceReadIOps:[] BlkioDeviceWriteIOps:[] CPUPeriod:0 CPUQuota:0 CPURealtimePeriod:0 CPURealtimeRuntime:0 CpusetCpus: CpusetMems: Devices:[] DeviceCgroupRules:[] DeviceRequests:[] KernelMemory:0 KernelMemoryTCP:0 MemoryReservation:0 MemorySwap:0 MemorySwappiness:0x228ace0e23c8 OomKillDisable:0x228ace0e22c3 PidsLimit:0x228ace0e2428 Ulimits:[] CPUCount:0 CPUPercent:0 IOMaximumIOps:0 IOMaximumBandwidth:0} Mounts:[{Type:volume Source:act-toolcache Target:/opt/hostedtoolcache ReadOnly:false Consistency: BindOptions:<nil> VolumeOptions:<nil> TmpfsOptions:<nil> ClusterOptions:<nil>} {Type:volume Source:GITEA-ACTIONS-TASK-1109_WORKFLOW-CI_JOB-build-env Target:/var/run/act ReadOnly:false Consistency: BindOptions:<nil> VolumeOptions:<nil> TmpfsOptions:<nil> ClusterOptions:<nil>} {Type:volume Source:GITEA-ACTIONS-TASK-1109_WORKFLOW-CI_JOB-build Target:/workspace/***/***3-server ReadOnly:false Consistency: BindOptions:<nil> VolumeOptions:<nil> TmpfsOptions:<nil> ClusterOptions:<nil>}] MaskedPaths:[] ReadonlyPaths:[] Init:<nil>}
2026-03-25T12:11:16.0650880Z Created container name=GITEA-ACTIONS-TASK-1109_WORKFLOW-CI_JOB-build id=81d7988ac62ef23d7fea4defd927f69654288f6f2a5992c3480ab61a65fc57f2 from image maven:3.9-eclipse-temurin-17 (platform: )
2026-03-25T12:11:16.0651415Z ENV ==> [RUNNER_TOOL_CACHE=/opt/hostedtoolcache RUNNER_OS=Linux RUNNER_ARCH=ARM64 RUNNER_TEMP=/tmp LANG=C.UTF-8]
2026-03-25T12:11:16.0651606Z 🐳 docker run image=maven:3.9-eclipse-temurin-17 platform= entrypoint=["/bin/sleep" "10800"] cmd=[] network="gitea_gitea_default"
2026-03-25T12:11:16.0651774Z Starting container: 81d7988ac62ef23d7fea4defd927f69654288f6f2a5992c3480ab61a65fc57f2
2026-03-25T12:11:16.2476626Z Started container: 81d7988ac62ef23d7fea4defd927f69654288f6f2a5992c3480ab61a65fc57f2
2026-03-25T12:11:16.3385957Z Writing entry to tarball workflow/event.json len:6148
2026-03-25T12:11:16.3386642Z Writing entry to tarball workflow/envs.txt len:0
2026-03-25T12:11:16.3386938Z Extracting content to '/var/run/act/'
2026-03-25T12:11:16.3535771Z ☁ git clone 'https://github.com/actions/checkout' # ref=v4
2026-03-25T12:11:16.3536162Z cloning https://github.com/actions/checkout to /root/.cache/act/c3fe249fe73091a17d6638fe1341e7bd0bcc3466ce52323c0688e83e2463a4ab
2026-03-25T12:11:16.6693512Z Non-terminating error while running 'git clone': some refs were not updated
2026-03-25T12:11:16.6760011Z ☁ git clone 'https://github.com/actions/cache' # ref=v4
2026-03-25T12:11:16.6760438Z cloning https://github.com/actions/cache to /root/.cache/act/6b4e4eb40e21c1bd02cb00a273f4d79af7c42205c1390e4e65c594ecd7a3696e
2026-03-25T12:11:17.5454691Z Non-terminating error while running 'git clone': some refs were not updated
2026-03-25T12:11:17.5674244Z evaluating expression ''
2026-03-25T12:11:17.5675000Z expression '' evaluated to 'true'
2026-03-25T12:11:17.5675244Z ⭐ Run Main Install Node.js 22
2026-03-25T12:11:17.5675584Z Writing entry to tarball workflow/outputcmd.txt len:0
2026-03-25T12:11:17.5675865Z Writing entry to tarball workflow/statecmd.txt len:0
2026-03-25T12:11:17.5676098Z Writing entry to tarball workflow/pathcmd.txt len:0
2026-03-25T12:11:17.5676311Z Writing entry to tarball workflow/envs.txt len:0
2026-03-25T12:11:17.5676498Z Writing entry to tarball workflow/SUMMARY.md len:0
2026-03-25T12:11:17.5676698Z Extracting content to '/var/run/act'
2026-03-25T12:11:17.5917925Z Wrote command \n\napt-get update && apt-get install -y ca-certificates curl gnupg\nmkdir -p /etc/apt/keyrings\ncurl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg\necho "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_22.x nodistro main" > /etc/apt/sources.list.d/nodesource.list\napt-get update && apt-get install -y nodejs\n\n\n to 'workflow/0.sh'
2026-03-25T12:11:17.5918846Z Writing entry to tarball workflow/0.sh len:407
2026-03-25T12:11:17.5919160Z Extracting content to '/var/run/act'
2026-03-25T12:11:17.5943770Z 🐳 docker exec cmd=[sh -e /var/run/act/workflow/0.sh] user= workdir=
2026-03-25T12:11:17.5944375Z Exec command '[sh -e /var/run/act/workflow/0.sh]'
2026-03-25T12:11:17.5944971Z Working directory '/workspace/***/***3-server'
2026-03-25T12:11:17.8888948Z Get:1 http://ports.ubuntu.com/ubuntu-ports noble InRelease [256 kB]
2026-03-25T12:11:18.4199917Z Get:2 http://ports.ubuntu.com/ubuntu-ports noble-updates InRelease [126 kB]
2026-03-25T12:11:18.5517055Z Get:3 http://ports.ubuntu.com/ubuntu-ports noble-backports InRelease [126 kB]
2026-03-25T12:11:18.6832646Z Get:4 http://ports.ubuntu.com/ubuntu-ports noble-security InRelease [126 kB]
2026-03-25T12:11:18.8159266Z Get:5 http://ports.ubuntu.com/ubuntu-ports noble/restricted arm64 Packages [113 kB]
2026-03-25T12:11:18.8552983Z Get:6 http://ports.ubuntu.com/ubuntu-ports noble/main arm64 Packages [1,776 kB]
2026-03-25T12:11:19.1280961Z Get:7 http://ports.ubuntu.com/ubuntu-ports noble/universe arm64 Packages [19.0 MB]
2026-03-25T12:11:19.6229227Z Get:8 http://ports.ubuntu.com/ubuntu-ports noble/multiverse arm64 Packages [274 kB]
2026-03-25T12:11:19.6298357Z Get:9 http://ports.ubuntu.com/ubuntu-ports noble-updates/restricted arm64 Packages [5,019 kB]
2026-03-25T12:11:19.7533806Z Get:10 http://ports.ubuntu.com/ubuntu-ports noble-updates/main arm64 Packages [2,451 kB]
2026-03-25T12:11:19.8087840Z Get:11 http://ports.ubuntu.com/ubuntu-ports noble-updates/universe arm64 Packages [2,057 kB]
2026-03-25T12:11:19.8574738Z Get:12 http://ports.ubuntu.com/ubuntu-ports noble-updates/multiverse arm64 Packages [45.7 kB]
2026-03-25T12:11:19.8583840Z Get:13 http://ports.ubuntu.com/ubuntu-ports noble-backports/universe arm64 Packages [36.1 kB]
2026-03-25T12:11:19.8587388Z Get:14 http://ports.ubuntu.com/ubuntu-ports noble-backports/main arm64 Packages [49.5 kB]
2026-03-25T12:11:19.8599308Z Get:15 http://ports.ubuntu.com/ubuntu-ports noble-backports/multiverse arm64 Packages [695 B]
2026-03-25T12:11:19.8601750Z Get:16 http://ports.ubuntu.com/ubuntu-ports noble-security/main arm64 Packages [2,087 kB]
2026-03-25T12:11:19.9076502Z Get:17 http://ports.ubuntu.com/ubuntu-ports noble-security/multiverse arm64 Packages [44.1 kB]
2026-03-25T12:11:19.9088113Z Get:18 http://ports.ubuntu.com/ubuntu-ports noble-security/restricted arm64 Packages [4,828 kB]
2026-03-25T12:11:20.0209834Z Get:19 http://ports.ubuntu.com/ubuntu-ports noble-security/universe arm64 Packages [1,502 kB]
2026-03-25T12:11:21.2001793Z Fetched 39.9 MB in 4s (11.4 MB/s)
2026-03-25T12:11:22.4920632Z Reading package lists...
2026-03-25T12:11:23.8283529Z Reading package lists...
2026-03-25T12:11:24.1797003Z Building dependency tree...
2026-03-25T12:11:24.1797885Z Reading state information...
2026-03-25T12:11:24.9189505Z ca-certificates is already the newest version (20240203).
2026-03-25T12:11:24.9190005Z curl is already the newest version (8.5.0-2ubuntu10.8).
2026-03-25T12:11:24.9190221Z gnupg is already the newest version (2.4.4-2ubuntu17.4).
2026-03-25T12:11:24.9190409Z 0 upgraded, 0 newly installed, 0 to remove and 18 not upgraded.
2026-03-25T12:11:25.2124240Z Get:1 https://deb.nodesource.com/node_22.x nodistro InRelease [12.1 kB]
2026-03-25T12:11:25.2904786Z Get:2 https://deb.nodesource.com/node_22.x nodistro/main arm64 Packages [9,040 B]
2026-03-25T12:11:34.8063450Z Hit:3 http://ports.ubuntu.com/ubuntu-ports noble InRelease
2026-03-25T12:11:34.8884900Z Hit:4 http://ports.ubuntu.com/ubuntu-ports noble-updates InRelease
2026-03-25T12:11:34.9713799Z Hit:5 http://ports.ubuntu.com/ubuntu-ports noble-backports InRelease
2026-03-25T12:11:35.3400058Z Hit:6 http://ports.ubuntu.com/ubuntu-ports noble-security InRelease
2026-03-25T12:11:35.3911975Z Fetched 21.2 kB in 10s (2,048 B/s)
2026-03-25T12:11:36.6884300Z Reading package lists...
2026-03-25T12:11:38.0360973Z Reading package lists...
2026-03-25T12:11:38.4220315Z Building dependency tree...
2026-03-25T12:11:38.4221340Z Reading state information...
2026-03-25T12:11:39.0941013Z The following additional packages will be installed:
2026-03-25T12:11:39.0960465Z libpython3-stdlib libpython3.12-minimal libpython3.12-stdlib media-types
2026-03-25T12:11:39.0966161Z netbase python3 python3-minimal python3.12 python3.12-minimal
2026-03-25T12:11:39.0998340Z Suggested packages:
2026-03-25T12:11:39.0998714Z python3-doc python3-tk python3-venv python3.12-venv python3.12-doc
2026-03-25T12:11:39.0998888Z binfmt-support
2026-03-25T12:11:39.1810008Z The following NEW packages will be installed:
2026-03-25T12:11:39.1823006Z libpython3-stdlib libpython3.12-minimal libpython3.12-stdlib media-types
2026-03-25T12:11:39.1830570Z netbase nodejs python3 python3-minimal python3.12 python3.12-minimal
2026-03-25T12:11:39.2816298Z 0 upgraded, 10 newly installed, 0 to remove and 18 not upgraded.
2026-03-25T12:11:39.2816843Z Need to get 42.9 MB of archives.
2026-03-25T12:11:39.2817042Z After this operation, 259 MB of additional disk space will be used.
2026-03-25T12:11:39.2817215Z Get:1 http://ports.ubuntu.com/ubuntu-ports noble-updates/main arm64 libpython3.12-minimal arm64 3.12.3-1ubuntu0.12 [834 kB]
2026-03-25T12:11:39.3903078Z Get:2 https://deb.nodesource.com/node_22.x nodistro/main arm64 nodejs arm64 22.22.1-1nodesource1 [37.0 MB]
2026-03-25T12:11:39.4744824Z Get:3 http://ports.ubuntu.com/ubuntu-ports noble-updates/main arm64 python3.12-minimal arm64 3.12.3-1ubuntu0.12 [2,252 kB]
2026-03-25T12:11:39.5305320Z Get:4 http://ports.ubuntu.com/ubuntu-ports noble-updates/main arm64 python3-minimal arm64 3.12.3-0ubuntu2.1 [27.4 kB]
2026-03-25T12:11:39.5309325Z Get:5 http://ports.ubuntu.com/ubuntu-ports noble/main arm64 media-types all 10.1.0 [27.5 kB]
2026-03-25T12:11:39.5330600Z Get:6 http://ports.ubuntu.com/ubuntu-ports noble/main arm64 netbase all 6.4 [13.1 kB]
2026-03-25T12:11:39.5333740Z Get:7 http://ports.ubuntu.com/ubuntu-ports noble-updates/main arm64 libpython3.12-stdlib arm64 3.12.3-1ubuntu0.12 [2,037 kB]
2026-03-25T12:11:39.5758776Z Get:8 http://ports.ubuntu.com/ubuntu-ports noble-updates/main arm64 python3.12 arm64 3.12.3-1ubuntu0.12 [651 kB]
2026-03-25T12:11:39.5835423Z Get:9 http://ports.ubuntu.com/ubuntu-ports noble-updates/main arm64 libpython3-stdlib arm64 3.12.3-0ubuntu2.1 [10.1 kB]
2026-03-25T12:11:39.5839250Z Get:10 http://ports.ubuntu.com/ubuntu-ports noble-updates/main arm64 python3 arm64 3.12.3-0ubuntu2.1 [23.0 kB]
2026-03-25T12:11:40.4405153Z debconf: delaying package configuration, since apt-utils is not installed
2026-03-25T12:11:40.5017505Z Fetched 42.9 MB in 1s (45.9 MB/s)
2026-03-25T12:11:40.5588037Z Selecting previously unselected package libpython3.12-minimal:arm64.
2026-03-25T12:11:40.5615337Z (Reading database ...
(Reading database ... 5%
(Reading database ... 10%
(Reading database ... 15%
(Reading database ... 20%
(Reading database ... 25%
(Reading database ... 30%
(Reading database ... 35%
(Reading database ... 40%
(Reading database ... 45%
(Reading database ... 50%
(Reading database ... 55%
(Reading database ... 60%
(Reading database ... 65%
(Reading database ... 70%
(Reading database ... 75%
(Reading database ... 80%
(Reading database ... 85%
(Reading database ... 90%
(Reading database ... 95%
(Reading database ... 100%
(Reading database ... 10203 files and directories currently installed.)
2026-03-25T12:11:40.5626546Z Preparing to unpack .../libpython3.12-minimal_3.12.3-1ubuntu0.12_arm64.deb ...
2026-03-25T12:11:40.5712786Z Unpacking libpython3.12-minimal:arm64 (3.12.3-1ubuntu0.12) ...
2026-03-25T12:11:40.7085040Z Selecting previously unselected package python3.12-minimal.
2026-03-25T12:11:40.7096226Z Preparing to unpack .../python3.12-minimal_3.12.3-1ubuntu0.12_arm64.deb ...
2026-03-25T12:11:40.7201720Z Unpacking python3.12-minimal (3.12.3-1ubuntu0.12) ...
2026-03-25T12:11:40.8107975Z Setting up libpython3.12-minimal:arm64 (3.12.3-1ubuntu0.12) ...
2026-03-25T12:11:40.8653673Z Setting up python3.12-minimal (3.12.3-1ubuntu0.12) ...
2026-03-25T12:11:41.9358111Z Selecting previously unselected package python3-minimal.
2026-03-25T12:11:41.9403560Z (Reading database ...
(Reading database ... 5%
(Reading database ... 10%
(Reading database ... 15%
(Reading database ... 20%
(Reading database ... 25%
(Reading database ... 30%
(Reading database ... 35%
(Reading database ... 40%
(Reading database ... 45%
(Reading database ... 50%
(Reading database ... 55%
(Reading database ... 60%
(Reading database ... 65%
(Reading database ... 70%
(Reading database ... 75%
(Reading database ... 80%
(Reading database ... 85%
(Reading database ... 90%
(Reading database ... 95%
(Reading database ... 100%
(Reading database ... 10514 files and directories currently installed.)
2026-03-25T12:11:41.9413961Z Preparing to unpack .../0-python3-minimal_3.12.3-0ubuntu2.1_arm64.deb ...
2026-03-25T12:11:41.9559244Z Unpacking python3-minimal (3.12.3-0ubuntu2.1) ...
2026-03-25T12:11:42.0307879Z Selecting previously unselected package media-types.
2026-03-25T12:11:42.0325837Z Preparing to unpack .../1-media-types_10.1.0_all.deb ...
2026-03-25T12:11:42.0457110Z Unpacking media-types (10.1.0) ...
2026-03-25T12:11:42.1145168Z Selecting previously unselected package netbase.
2026-03-25T12:11:42.1159169Z Preparing to unpack .../2-netbase_6.4_all.deb ...
2026-03-25T12:11:42.1242901Z Unpacking netbase (6.4) ...
2026-03-25T12:11:42.1792726Z Selecting previously unselected package libpython3.12-stdlib:arm64.
2026-03-25T12:11:42.1793740Z Preparing to unpack .../3-libpython3.12-stdlib_3.12.3-1ubuntu0.12_arm64.deb ...
2026-03-25T12:11:42.1880816Z Unpacking libpython3.12-stdlib:arm64 (3.12.3-1ubuntu0.12) ...
2026-03-25T12:11:42.3101823Z Selecting previously unselected package python3.12.
2026-03-25T12:11:42.3113846Z Preparing to unpack .../4-python3.12_3.12.3-1ubuntu0.12_arm64.deb ...
2026-03-25T12:11:42.3201402Z Unpacking python3.12 (3.12.3-1ubuntu0.12) ...
2026-03-25T12:11:42.3772802Z Selecting previously unselected package libpython3-stdlib:arm64.
2026-03-25T12:11:42.3773312Z Preparing to unpack .../5-libpython3-stdlib_3.12.3-0ubuntu2.1_arm64.deb ...
2026-03-25T12:11:42.3858444Z Unpacking libpython3-stdlib:arm64 (3.12.3-0ubuntu2.1) ...
2026-03-25T12:11:42.4387555Z Setting up python3-minimal (3.12.3-0ubuntu2.1) ...
2026-03-25T12:11:42.6614832Z Selecting previously unselected package python3.
2026-03-25T12:11:42.6659520Z (Reading database ...
(Reading database ... 5%
(Reading database ... 10%
(Reading database ... 15%
(Reading database ... 20%
(Reading database ... 25%
(Reading database ... 30%
(Reading database ... 35%
(Reading database ... 40%
(Reading database ... 45%
(Reading database ... 50%
(Reading database ... 55%
(Reading database ... 60%
(Reading database ... 65%
(Reading database ... 70%
(Reading database ... 75%
(Reading database ... 80%
(Reading database ... 85%
(Reading database ... 90%
(Reading database ... 95%
(Reading database ... 100%
(Reading database ... 10955 files and directories currently installed.)
2026-03-25T12:11:42.6670209Z Preparing to unpack .../python3_3.12.3-0ubuntu2.1_arm64.deb ...
2026-03-25T12:11:42.6782622Z Unpacking python3 (3.12.3-0ubuntu2.1) ...
2026-03-25T12:11:42.7536426Z Selecting previously unselected package nodejs.
2026-03-25T12:11:42.7543156Z Preparing to unpack .../nodejs_22.22.1-1nodesource1_arm64.deb ...
2026-03-25T12:11:42.7640439Z Unpacking nodejs (22.22.1-1nodesource1) ...
2026-03-25T12:11:44.5533574Z Setting up media-types (10.1.0) ...
2026-03-25T12:11:44.6116920Z Setting up netbase (6.4) ...
2026-03-25T12:11:44.7126932Z Setting up libpython3.12-stdlib:arm64 (3.12.3-1ubuntu0.12) ...
2026-03-25T12:11:44.7537181Z Setting up python3.12 (3.12.3-1ubuntu0.12) ...
2026-03-25T12:11:45.9736127Z Setting up libpython3-stdlib:arm64 (3.12.3-0ubuntu2.1) ...
2026-03-25T12:11:45.9995094Z Setting up python3 (3.12.3-0ubuntu2.1) ...
2026-03-25T12:11:46.0202105Z running python rtupdate hooks for python3.12...
2026-03-25T12:11:46.0202749Z running python post-rtupdate hooks for python3.12...
2026-03-25T12:11:46.1647503Z Setting up nodejs (22.22.1-1nodesource1) ...
2026-03-25T12:11:46.6406508Z ::add-matcher::/run/act/actions/c3fe249fe73091a17d6638fe1341e7bd0bcc3466ce52323c0688e83e2463a4ab/dist/problem-matcher.json
2026-03-25T12:11:46.6410499Z Syncing repository: ***/***3-server
2026-03-25T12:11:46.6414140Z ::group::Getting Git version info
2026-03-25T12:11:46.6414740Z Working directory is '/workspace/***/***3-server'
2026-03-25T12:11:46.6451296Z [command]/usr/bin/git version
2026-03-25T12:11:46.6498415Z git version 2.43.0
2026-03-25T12:11:46.6528997Z ::endgroup::
2026-03-25T12:11:46.6559915Z Temporarily overriding HOME='/tmp/b1eaf53a-eeb8-4afe-bb1b-963811cdef44' before making global git config changes
2026-03-25T12:11:46.6560661Z Adding repository directory to the temporary git global config as a safe directory
2026-03-25T12:11:46.6569698Z [command]/usr/bin/git config --global --add safe.directory /workspace/***/***3-server
2026-03-25T12:11:46.6604482Z Deleting the contents of '/workspace/***/***3-server'
2026-03-25T12:11:46.6609411Z ::group::Initializing the repository
2026-03-25T12:11:46.6614132Z [command]/usr/bin/git init /workspace/***/***3-server
2026-03-25T12:11:46.6647197Z hint: Using 'master' as the name for the initial branch. This default branch name
2026-03-25T12:11:46.6647668Z hint: is subject to change. To configure the initial branch name to use in all
2026-03-25T12:11:46.6647876Z hint: of your new repositories, which will suppress this warning, call:
2026-03-25T12:11:46.6648031Z hint:
2026-03-25T12:11:46.6648344Z hint: git config --global init.defaultBranch <name>
2026-03-25T12:11:46.6648485Z hint:
2026-03-25T12:11:46.6648606Z hint: Names commonly chosen instead of 'master' are 'main', 'trunk' and
2026-03-25T12:11:46.6648745Z hint: 'development'. The just-created branch can be renamed via this command:
2026-03-25T12:11:46.6648895Z hint:
2026-03-25T12:11:46.6649012Z hint: git branch -m <name>
2026-03-25T12:11:46.6655581Z Initialized empty Git repository in /workspace/***/***3-server/.git/
2026-03-25T12:11:46.6673484Z [command]/usr/bin/git remote add origin https://gitea.siegeln.net/***/***3-server
2026-03-25T12:11:46.6705963Z ::endgroup::
2026-03-25T12:11:46.6706313Z ::group::Disabling automatic garbage collection
2026-03-25T12:11:46.6711562Z [command]/usr/bin/git config --local gc.auto 0
2026-03-25T12:11:46.6742385Z ::endgroup::
2026-03-25T12:11:46.6742714Z ::group::Setting up auth
2026-03-25T12:11:46.6753437Z [command]/usr/bin/git config --local --name-only --get-regexp core\.sshCommand
2026-03-25T12:11:46.6785919Z [command]/usr/bin/git submodule foreach --recursive sh -c "git config --local --name-only --get-regexp 'core\.sshCommand' && git config --local --unset-all 'core.sshCommand' || :"
2026-03-25T12:11:46.6964779Z [command]/usr/bin/git config --local --name-only --get-regexp http\.https\:\/\/gitea\.siegeln\.net\/\.extraheader
2026-03-25T12:11:46.6996612Z [command]/usr/bin/git submodule foreach --recursive sh -c "git config --local --name-only --get-regexp 'http\.https\:\/\/gitea\.siegeln\.net\/\.extraheader' && git config --local --unset-all 'http.https://gitea.siegeln.net/.extraheader' || :"
2026-03-25T12:11:46.7177068Z [command]/usr/bin/git config --local --name-only --get-regexp ^includeIf\.gitdir:
2026-03-25T12:11:46.7209537Z [command]/usr/bin/git submodule foreach --recursive git config --local --show-origin --name-only --get-regexp remote.origin.url
2026-03-25T12:11:46.7386392Z [command]/usr/bin/git config --local http.https://gitea.siegeln.net/.extraheader AUTHORIZATION: basic ***
2026-03-25T12:11:46.7420093Z ::endgroup::
2026-03-25T12:11:46.7420425Z ::group::Fetching the repository
2026-03-25T12:11:46.7444311Z [command]/usr/bin/git -c protocol.version=2 fetch --no-tags --prune --no-recurse-submodules --depth=1 origin +7fd55ea8ba92830ddad59dd3fe3d9a0516254946:refs/remotes/origin/main
2026-03-25T12:11:47.0408153Z From https://gitea.siegeln.net/***/***3-server
2026-03-25T12:11:47.0408743Z * [new ref] 7fd55ea8ba92830ddad59dd3fe3d9a0516254946 -> origin/main
2026-03-25T12:11:47.0425449Z ::endgroup::
2026-03-25T12:11:47.0425869Z ::group::Determining the checkout info
2026-03-25T12:11:47.0428330Z ::endgroup::
2026-03-25T12:11:47.0434686Z [command]/usr/bin/git sparse-checkout disable
2026-03-25T12:11:47.0468001Z [command]/usr/bin/git config --local --unset-all extensions.worktreeConfig
2026-03-25T12:11:47.0494380Z ::group::Checking out the ref
2026-03-25T12:11:47.0499182Z [command]/usr/bin/git checkout --progress --force -B main refs/remotes/origin/main
2026-03-25T12:11:47.0799235Z Switched to a new branch 'main'
2026-03-25T12:11:47.0801974Z branch 'main' set up to track 'origin/main'.
2026-03-25T12:11:47.0807273Z ::endgroup::
2026-03-25T12:11:47.0851193Z [command]/usr/bin/git log -1 --format=%H
2026-03-25T12:11:47.0873738Z 7fd55ea8ba92830ddad59dd3fe3d9a0516254946
2026-03-25T12:11:47.0889884Z ::remove-matcher owner=checkout-git::
2026-03-25T12:11:47.9031005Z (node:674) [DEP0040] DeprecationWarning: The `punycode` module is deprecated. Please use a userland alternative instead.
2026-03-25T12:11:47.9031552Z (Use `node --trace-deprecation ...` to show where the warning was created)
2026-03-25T12:11:48.7715362Z Cache Size: ~352 MB (368583412 B)
2026-03-25T12:11:48.7746030Z [command]/usr/bin/tar -xf /tmp/0d28713b-77ea-40d9-ac8d-19c5adfc0c5d/cache.tgz -P -C /workspace/***/***3-server -z
2026-03-25T12:11:52.3545912Z Cache restored successfully
2026-03-25T12:11:52.3981691Z Cache restored from key: linux-maven-9dd3df74bd8aba32251bb49c59e820a2aa73582b0f09ca6d6e930542cfb05391
2026-03-25T12:11:59.0472225Z
2026-03-25T12:11:59.0473793Z added 213 packages, and audited 214 packages in 6s
2026-03-25T12:11:59.0474257Z
2026-03-25T12:11:59.0474791Z 58 packages are looking for funding
2026-03-25T12:11:59.0474965Z run `npm fund` for details
2026-03-25T12:11:59.0493731Z
2026-03-25T12:11:59.0494280Z found 0 vulnerabilities
2026-03-25T12:11:59.2308378Z
2026-03-25T12:11:59.2309072Z > ui@0.0.0 build
2026-03-25T12:11:59.2309247Z > tsc -p tsconfig.app.json --noEmit && vite build
2026-03-25T12:11:59.2309401Z
2026-03-25T12:12:05.6744989Z vite v8.0.1 building client environment for production...
2026-03-25T12:12:06.3075689Z 
transforming...✓ 129 modules transformed.
2026-03-25T12:12:06.4496363Z rendering chunks...
2026-03-25T12:12:06.8014982Z computing gzip size...
2026-03-25T12:12:06.8346038Z dist/index.html 0.94 kB │ gzip: 0.41 kB
2026-03-25T12:12:06.8346660Z dist/assets/dm-sans-600-Aqo67rzb.woff2 14.14 kB
2026-03-25T12:12:06.8346848Z dist/assets/dm-sans-400-CW0RaeGs.woff2 14.20 kB
2026-03-25T12:12:06.8347005Z dist/assets/dm-sans-500-B9HHJjqV.woff2 14.30 kB
2026-03-25T12:12:06.8347232Z dist/assets/dm-sans-700-DvUfVpUG.woff2 14.34 kB
2026-03-25T12:12:06.8347378Z dist/assets/dm-sans-400-italic-DRLHr0TN.woff2 15.14 kB
2026-03-25T12:12:06.8347520Z dist/assets/jetbrains-mono-400-V6pRDFza.woff2 21.16 kB
2026-03-25T12:12:06.8347661Z dist/assets/jetbrains-mono-500-BWZEU5yA.woff2 21.83 kB
2026-03-25T12:12:06.8347799Z dist/assets/jetbrains-mono-600-C8RAYTDA.woff2 21.86 kB
2026-03-25T12:12:06.8347942Z dist/assets/OidcConfigPage-Bq9_k9q2.css 0.61 kB │ gzip: 0.31 kB
2026-03-25T12:12:06.8348093Z dist/assets/AuditLogPage-wcUcdzwn.css 1.19 kB │ gzip: 0.56 kB
2026-03-25T12:12:06.8348237Z dist/assets/RoutesMetrics-DaUBduav.css 1.46 kB │ gzip: 0.62 kB
2026-03-25T12:12:06.8348390Z dist/assets/RbacPage-5iGc4gch.css 1.98 kB │ gzip: 0.69 kB
2026-03-25T12:12:06.8348537Z dist/assets/AgentInstance--Z0IKMF0.css 2.50 kB │ gzip: 0.75 kB
2026-03-25T12:12:06.8348713Z dist/assets/AgentHealth-y0_55tmK.css 3.42 kB │ gzip: 1.04 kB
2026-03-25T12:12:06.8348861Z dist/assets/Dashboard-BYYJXEtx.css 3.64 kB │ gzip: 1.13 kB
2026-03-25T12:12:06.8349210Z dist/assets/RouteDetail-CjL6IMio.css 4.26 kB │ gzip: 1.09 kB
2026-03-25T12:12:06.8349349Z dist/assets/ExchangeDetail-VcItziWb.css 6.78 kB │ gzip: 1.67 kB
2026-03-25T12:12:06.8349503Z dist/assets/index-CXegvd7B.css 111.14 kB │ gzip: 18.21 kB
2026-03-25T12:12:06.8349645Z dist/assets/SwaggerPage-CH1CJXTy.css 176.70 kB │ gzip: 26.18 kB
2026-03-25T12:12:06.8349786Z dist/assets/use-refresh-interval-CBD44ngw.js 0.19 kB │ gzip: 0.15 kB
2026-03-25T12:12:06.8349931Z dist/assets/admin-api-Dldh4fYm.js 0.49 kB │ gzip: 0.35 kB
2026-03-25T12:12:06.8350083Z dist/assets/AdminLayout-CKg4TLhS.js 0.55 kB │ gzip: 0.34 kB
2026-03-25T12:12:06.8350288Z dist/assets/chunk-B3K2TuZy.js 0.55 kB │ gzip: 0.35 kB
2026-03-25T12:12:06.8350433Z dist/assets/agent-metrics-DI4HNJNm.js 0.59 kB │ gzip: 0.42 kB
2026-03-25T12:12:06.8350571Z dist/assets/SwaggerPage-ZwDCrEj1.js 0.87 kB │ gzip: 0.54 kB
2026-03-25T12:12:06.8350710Z dist/assets/diagram-mapping-zsjZZKRP.js 1.33 kB │ gzip: 0.68 kB
2026-03-25T12:12:06.8350879Z dist/assets/useMutation-CjGnzIYg.js 2.31 kB │ gzip: 0.97 kB
2026-03-25T12:12:06.8351033Z dist/assets/OpenSearchAdminPage-WnM4gikq.js 2.92 kB │ gzip: 1.17 kB
2026-03-25T12:12:06.8351173Z dist/assets/DatabaseAdminPage-CclQJ0Ok.js 3.09 kB │ gzip: 1.16 kB
2026-03-25T12:12:06.8351328Z dist/assets/AuditLogPage-4V3oK99a.js 4.53 kB │ gzip: 1.70 kB
2026-03-25T12:12:06.8351477Z dist/assets/OidcConfigPage-D0YsPv3U.js 5.38 kB │ gzip: 1.90 kB
2026-03-25T12:12:06.8351642Z dist/assets/RoutesMetrics-5XMqf33P.js 5.98 kB │ gzip: 2.10 kB
2026-03-25T12:12:06.8351804Z dist/assets/jsx-runtime-DdsoV0Vr.js 9.07 kB │ gzip: 3.52 kB
2026-03-25T12:12:06.8351939Z dist/assets/AgentInstance-Bzs6lRBr.js 9.86 kB │ gzip: 2.83 kB
2026-03-25T12:12:06.8352077Z dist/assets/Dashboard-C23kCbUs.js 10.16 kB │ gzip: 3.28 kB
2026-03-25T12:12:06.8352236Z dist/assets/auth-store-DCBo9Ans.js 10.34 kB │ gzip: 3.79 kB
2026-03-25T12:12:06.8352399Z dist/assets/AgentHealth-DwOu1dM9.js 11.36 kB │ gzip: 3.28 kB
2026-03-25T12:12:06.8352543Z dist/assets/RouteDetail-l02TOYKF.js 12.35 kB │ gzip: 4.07 kB
2026-03-25T12:12:06.8352685Z dist/assets/ExchangeDetail-BLpXn9YT.js 14.56 kB │ gzip: 4.25 kB
2026-03-25T12:12:06.8352827Z dist/assets/useQuery-oR6RVwVH.js 22.31 kB │ gzip: 7.37 kB
2026-03-25T12:12:06.8352968Z dist/assets/RbacPage-oz3jLDxG.js 25.49 kB │ gzip: 5.57 kB
2026-03-25T12:12:06.8353149Z dist/assets/index-COBtXOCg.js 205.44 kB │ gzip: 64.73 kB
2026-03-25T12:12:06.8353297Z dist/assets/index.es-BideRJQn.js 216.16 kB │ gzip: 66.22 kB
2026-03-25T12:12:06.8353439Z dist/assets/swagger-ui-bundle-BoDRmAdn.js 1,371.99 kB │ gzip: 390.77 kB
2026-03-25T12:12:06.8353572Z
2026-03-25T12:12:06.8353740Z ✓ built in 1.16s
2026-03-25T12:12:06.8356107Z [plugin builtin:vite-reporter]
2026-03-25T12:12:06.8356389Z (!) Some chunks are larger than 500 kB after minification. Consider:
2026-03-25T12:12:06.8356581Z - Using dynamic import() to code-split the application
2026-03-25T12:12:06.8356726Z - Use build.rolldownOptions.output.codeSplitting to improve chunking: https://rolldown.rs/reference/OutputOptions.codeSplitting
2026-03-25T12:12:06.8356917Z - Adjust chunk size limit for this warning via build.chunkSizeWarningLimit.
2026-03-25T12:12:08.8592085Z [INFO] Scanning for projects...
2026-03-25T12:12:09.3994902Z [INFO] ------------------------------------------------------------------------
2026-03-25T12:12:09.3995662Z [INFO] Reactor Build Order:
2026-03-25T12:12:09.3995968Z [INFO]
2026-03-25T12:12:09.4015044Z [INFO] Cameleer3 Server Parent [pom]
2026-03-25T12:12:09.4016943Z [INFO] Cameleer3 Server Core [jar]
2026-03-25T12:12:09.4020267Z [INFO] Cameleer3 Server App [jar]
2026-03-25T12:12:09.4204897Z [INFO]
2026-03-25T12:12:09.4206974Z [INFO] ---------------< com.***3:***3-server-parent >----------------
2026-03-25T12:12:09.4209665Z [INFO] Building Cameleer3 Server Parent 1.0-SNAPSHOT [1/3]
2026-03-25T12:12:09.4213444Z [INFO] from pom.xml
2026-03-25T12:12:09.4215425Z [INFO] --------------------------------[ pom ]---------------------------------
2026-03-25T12:12:09.4758836Z [INFO]
2026-03-25T12:12:09.4761722Z [INFO] --- clean:3.4.1:clean (default-clean) @ ***3-server-parent ---
2026-03-25T12:12:09.5985349Z [INFO]
2026-03-25T12:12:09.5998691Z [INFO] ----------------< com.***3:***3-server-core >-----------------
2026-03-25T12:12:09.5999367Z [INFO] Building Cameleer3 Server Core 1.0-SNAPSHOT [2/3]
2026-03-25T12:12:09.5999608Z [INFO] from ***3-server-core/pom.xml
2026-03-25T12:12:09.5999870Z [INFO] --------------------------------[ jar ]---------------------------------
2026-03-25T12:12:09.9264378Z [INFO]
2026-03-25T12:12:09.9265012Z [INFO] --- clean:3.4.1:clean (default-clean) @ ***3-server-core ---
2026-03-25T12:12:09.9294155Z [INFO]
2026-03-25T12:12:09.9294802Z [INFO] --- resources:3.3.1:resources (default-resources) @ ***3-server-core ---
2026-03-25T12:12:10.1415949Z [INFO] skip non existing resourceDirectory /workspace/***/***3-server/***3-server-core/src/main/resources
2026-03-25T12:12:10.1416720Z [INFO] skip non existing resourceDirectory /workspace/***/***3-server/***3-server-core/src/main/resources
2026-03-25T12:12:10.1433945Z [INFO]
2026-03-25T12:12:10.1434657Z [INFO] --- compiler:3.13.0:compile (default-compile) @ ***3-server-core ---
2026-03-25T12:12:10.3640938Z [INFO] Recompiling the module because of changed source code.
2026-03-25T12:12:10.3728671Z [INFO] Compiling 62 source files with javac [debug parameters release 17] to target/classes
2026-03-25T12:12:12.5455820Z [INFO]
2026-03-25T12:12:12.5458698Z [INFO] --- resources:3.3.1:testResources (default-testResources) @ ***3-server-core ---
2026-03-25T12:12:12.5545314Z [INFO] skip non existing resourceDirectory /workspace/***/***3-server/***3-server-core/src/test/resources
2026-03-25T12:12:12.5551351Z [INFO]
2026-03-25T12:12:12.5554792Z [INFO] --- compiler:3.13.0:testCompile (default-testCompile) @ ***3-server-core ---
2026-03-25T12:12:12.5684758Z [INFO] Recompiling the module because of changed dependency.
2026-03-25T12:12:12.5695664Z [INFO] Compiling 3 source files with javac [debug parameters release 17] to target/test-classes
2026-03-25T12:12:13.8627365Z [INFO]
2026-03-25T12:12:13.8629352Z [INFO] --- surefire:3.5.2:test (default-test) @ ***3-server-core ---
2026-03-25T12:12:14.1009777Z [INFO] Using auto detected provider org.apache.maven.surefire.junitplatform.JUnitPlatformProvider
2026-03-25T12:12:14.1865594Z [INFO]
2026-03-25T12:12:14.1866264Z [INFO] -------------------------------------------------------
2026-03-25T12:12:14.1866544Z [INFO] T E S T S
2026-03-25T12:12:14.1867788Z [INFO] -------------------------------------------------------
2026-03-25T12:12:15.2551472Z [INFO] Running com.***3.server.core.detail.TreeReconstructionTest
2026-03-25T12:12:15.5724418Z OpenJDK 64-Bit Server VM warning: Sharing is only supported for boot loader classes because bootstrap classpath has been appended
2026-03-25T12:12:16.6140778Z [INFO] Tests run: 4, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 1.313 s -- in com.***3.server.core.detail.TreeReconstructionTest
2026-03-25T12:12:16.6183288Z [INFO] Running com.***3.server.core.agent.AgentRegistryServiceTest
2026-03-25T12:12:16.6190775Z [INFO] Running com.***3.server.core.agent.AgentRegistryServiceTest$Commands
2026-03-25T12:12:16.6263438Z SLF4J(W): No SLF4J providers were found.
2026-03-25T12:12:16.6264169Z SLF4J(W): Defaulting to no-operation (NOP) logger implementation
2026-03-25T12:12:16.6264406Z SLF4J(W): See https://www.slf4j.org/codes.html#noProviders for further details.
2026-03-25T12:12:16.6999152Z [INFO] Tests run: 8, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.104 s -- in com.***3.server.core.agent.AgentRegistryServiceTest$Commands
2026-03-25T12:12:16.7024457Z [INFO] Running com.***3.server.core.agent.AgentRegistryServiceTest$Queries
2026-03-25T12:12:16.7186966Z [INFO] Tests run: 4, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.028 s -- in com.***3.server.core.agent.AgentRegistryServiceTest$Queries
2026-03-25T12:12:16.7189398Z [INFO] Running com.***3.server.core.agent.AgentRegistryServiceTest$LifecycleTransitions
2026-03-25T12:12:16.7933600Z [INFO] Tests run: 4, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.066 s -- in com.***3.server.core.agent.AgentRegistryServiceTest$LifecycleTransitions
2026-03-25T12:12:16.7944637Z [INFO] Running com.***3.server.core.agent.AgentRegistryServiceTest$Heartbeat
2026-03-25T12:12:16.8157231Z [INFO] Tests run: 4, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.015 s -- in com.***3.server.core.agent.AgentRegistryServiceTest$Heartbeat
2026-03-25T12:12:16.8190493Z [INFO] Running com.***3.server.core.agent.AgentRegistryServiceTest$Registration
2026-03-25T12:12:16.8289762Z [INFO] Tests run: 3, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.020 s -- in com.***3.server.core.agent.AgentRegistryServiceTest$Registration
2026-03-25T12:12:16.8405658Z [INFO] Tests run: 0, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.254 s -- in com.***3.server.core.agent.AgentRegistryServiceTest
2026-03-25T12:12:16.8433351Z [INFO] Running com.***3.server.core.ingestion.WriteBufferTest
2026-03-25T12:12:16.8748334Z [INFO] Tests run: 10, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.041 s -- in com.***3.server.core.ingestion.WriteBufferTest
2026-03-25T12:12:16.8990584Z [INFO]
2026-03-25T12:12:16.8991202Z [INFO] Results:
2026-03-25T12:12:16.8991549Z [INFO]
2026-03-25T12:12:16.8996481Z [INFO] Tests run: 37, Failures: 0, Errors: 0, Skipped: 0
2026-03-25T12:12:16.8997041Z [INFO]
2026-03-25T12:12:16.9036244Z [INFO]
2026-03-25T12:12:16.9036930Z [INFO] --- jar:3.4.2:jar (default-jar) @ ***3-server-core ---
2026-03-25T12:12:17.3553610Z [INFO] Building jar: /workspace/***/***3-server/***3-server-core/target/***3-server-core-1.0-SNAPSHOT.jar
2026-03-25T12:12:17.4343059Z [INFO]
2026-03-25T12:12:17.4343761Z [INFO] -----------------< com.***3:***3-server-app >-----------------
2026-03-25T12:12:17.4344006Z [INFO] Building Cameleer3 Server App 1.0-SNAPSHOT [3/3]
2026-03-25T12:12:17.4346242Z [INFO] from ***3-server-app/pom.xml
2026-03-25T12:12:17.4347591Z [INFO] --------------------------------[ jar ]---------------------------------
2026-03-25T12:12:18.7737910Z [INFO]
2026-03-25T12:12:18.7738533Z [INFO] --- clean:3.4.1:clean (default-clean) @ ***3-server-app ---
2026-03-25T12:12:18.7770445Z [INFO]
2026-03-25T12:12:18.7771175Z [INFO] --- resources:3.3.1:copy-resources (copy-ui-dist) @ ***3-server-app ---
2026-03-25T12:12:18.7925285Z [INFO] Copying 48 resources from ../ui/dist to target/classes/static
2026-03-25T12:12:18.8155069Z [INFO]
2026-03-25T12:12:18.8156631Z [INFO] --- resources:3.3.1:resources (default-resources) @ ***3-server-app ---
2026-03-25T12:12:18.8197148Z [INFO] Copying 1 resource from src/main/resources to target/classes
2026-03-25T12:12:18.8639638Z [INFO] Copying 4 resources from src/main/resources to target/classes
2026-03-25T12:12:18.8640395Z [INFO]
2026-03-25T12:12:18.8640613Z [INFO] --- compiler:3.13.0:compile (default-compile) @ ***3-server-app ---
2026-03-25T12:12:18.8844470Z [INFO] Recompiling the module because of changed dependency.
2026-03-25T12:12:18.8848893Z [INFO] Compiling 104 source files with javac [debug parameters release 17] to target/classes
2026-03-25T12:12:21.8486343Z [INFO] /workspace/***/***3-server/***3-server-app/src/main/java/com/***3/server/app/security/OidcTokenExchanger.java: /workspace/***/***3-server/***3-server-app/src/main/java/com/***3/server/app/security/OidcTokenExchanger.java uses or overrides a deprecated API.
2026-03-25T12:12:21.8487241Z [INFO] /workspace/***/***3-server/***3-server-app/src/main/java/com/***3/server/app/security/OidcTokenExchanger.java: Recompile with -Xlint:deprecation for details.
2026-03-25T12:12:21.8487541Z [INFO] -------------------------------------------------------------
2026-03-25T12:12:21.8487894Z [ERROR] COMPILATION ERROR :
2026-03-25T12:12:21.8488157Z [INFO] -------------------------------------------------------------
2026-03-25T12:12:21.8488337Z [ERROR] /workspace/***/***3-server/***3-server-app/src/main/java/com/***3/server/app/controller/LogIngestionController.java:[3,34] cannot find symbol
2026-03-25T12:12:21.8488560Z symbol: class LogBatch
2026-03-25T12:12:21.8488701Z location: package com.***3.common.model
2026-03-25T12:12:21.8488975Z [ERROR] /workspace/***/***3-server/***3-server-app/src/main/java/com/***3/server/app/search/OpenSearchLogIndex.java:[3,34] cannot find symbol
2026-03-25T12:12:21.8489229Z symbol: class LogEntry
2026-03-25T12:12:21.8489369Z location: package com.***3.common.model
2026-03-25T12:12:21.8489585Z [ERROR] /workspace/***/***3-server/***3-server-app/src/main/java/com/***3/server/app/controller/LogIngestionController.java:[40,57] cannot find symbol
2026-03-25T12:12:21.8489887Z symbol: class LogBatch
2026-03-25T12:12:21.8490086Z location: class com.***3.server.app.controller.LogIngestionController
2026-03-25T12:12:21.8490358Z [ERROR] /workspace/***/***3-server/***3-server-app/src/main/java/com/***3/server/app/search/OpenSearchLogIndex.java:[94,69] cannot find symbol
2026-03-25T12:12:21.8490581Z symbol: class LogEntry
2026-03-25T12:12:21.8490739Z location: class com.***3.server.app.search.OpenSearchLogIndex
2026-03-25T12:12:21.8490944Z [ERROR] /workspace/***/***3-server/***3-server-app/src/main/java/com/***3/server/app/search/OpenSearchLogIndex.java:[135,39] cannot find symbol
2026-03-25T12:12:21.8491180Z symbol: class LogEntry
2026-03-25T12:12:21.8491349Z location: class com.***3.server.app.search.OpenSearchLogIndex
2026-03-25T12:12:21.8491573Z [ERROR] /workspace/***/***3-server/***3-server-app/src/main/java/com/***3/server/app/search/OpenSearchLogIndex.java:[102,18] cannot find symbol
2026-03-25T12:12:21.8491783Z symbol: class LogEntry
2026-03-25T12:12:21.8491947Z location: class com.***3.server.app.search.OpenSearchLogIndex
2026-03-25T12:12:21.8492176Z [INFO] 6 errors
2026-03-25T12:12:21.8492385Z [INFO] -------------------------------------------------------------
2026-03-25T12:12:21.8502503Z [INFO] ------------------------------------------------------------------------
2026-03-25T12:12:21.8503274Z [INFO] Reactor Summary for Cameleer3 Server Parent 1.0-SNAPSHOT:
2026-03-25T12:12:21.8503850Z [INFO]
2026-03-25T12:12:21.8524175Z [INFO] Cameleer3 Server Parent ............................ SUCCESS [ 0.178 s]
2026-03-25T12:12:21.8524906Z [INFO] Cameleer3 Server Core .............................. SUCCESS [ 7.835 s]
2026-03-25T12:12:21.8525142Z [INFO] Cameleer3 Server App ............................... FAILURE [ 4.416 s]
2026-03-25T12:12:21.8525315Z [INFO] ------------------------------------------------------------------------
2026-03-25T12:12:21.8525608Z [INFO] BUILD FAILURE
2026-03-25T12:12:21.8525797Z [INFO] ------------------------------------------------------------------------
2026-03-25T12:12:21.8526046Z [INFO] Total time: 13.033 s
2026-03-25T12:12:21.8526239Z [INFO] Finished at: 2026-03-25T12:12:21Z
2026-03-25T12:12:21.8526470Z [INFO] ------------------------------------------------------------------------
2026-03-25T12:12:21.8539577Z [ERROR] Failed to execute goal org.apache.maven.plugins:maven-compiler-plugin:3.13.0:compile (default-compile) on project ***3-server-app: Compilation failure: Compilation failure:
2026-03-25T12:12:21.8540261Z [ERROR] /workspace/***/***3-server/***3-server-app/src/main/java/com/***3/server/app/controller/LogIngestionController.java:[3,34] cannot find symbol
2026-03-25T12:12:21.8540576Z [ERROR] symbol: class LogBatch
2026-03-25T12:12:21.8540792Z [ERROR] location: package com.***3.common.model
2026-03-25T12:12:21.8541144Z [ERROR] /workspace/***/***3-server/***3-server-app/src/main/java/com/***3/server/app/search/OpenSearchLogIndex.java:[3,34] cannot find symbol
2026-03-25T12:12:21.8541428Z [ERROR] symbol: class LogEntry
2026-03-25T12:12:21.8541596Z [ERROR] location: package com.***3.common.model
2026-03-25T12:12:21.8541880Z [ERROR] /workspace/***/***3-server/***3-server-app/src/main/java/com/***3/server/app/controller/LogIngestionController.java:[40,57] cannot find symbol
2026-03-25T12:12:21.8542178Z [ERROR] symbol: class LogBatch
2026-03-25T12:12:21.8542402Z [ERROR] location: class com.***3.server.app.controller.LogIngestionController
2026-03-25T12:12:21.8542619Z [ERROR] /workspace/***/***3-server/***3-server-app/src/main/java/com/***3/server/app/search/OpenSearchLogIndex.java:[94,69] cannot find symbol
2026-03-25T12:12:21.8542855Z [ERROR] symbol: class LogEntry
2026-03-25T12:12:21.8543160Z [ERROR] location: class com.***3.server.app.search.OpenSearchLogIndex
2026-03-25T12:12:21.8543969Z [ERROR] /workspace/***/***3-server/***3-server-app/src/main/java/com/***3/server/app/search/OpenSearchLogIndex.java:[135,39] cannot find symbol
2026-03-25T12:12:21.8544642Z [ERROR] symbol: class LogEntry
2026-03-25T12:12:21.8544881Z [ERROR] location: class com.***3.server.app.search.OpenSearchLogIndex

View File

@@ -0,0 +1,11 @@
# Design System Update Instructions
Status: Both changes below were implemented in `@cameleer/design-system` v0.1.19 and consumed by cameleer3-server.
## 1. Sidebar: `onNavigate` callback prop — DONE (v0.1.19)
Sidebar now accepts `onNavigate?: (path: string) => void`. When provided, sidebar calls `onNavigate(path)` instead of using internal `<Link>` navigation. The server UI translates paths to its own URL structure.
## 2. DataTable: `fillHeight` prop — DONE (v0.1.19)
DataTable now accepts `fillHeight?: boolean`. When true, the table fills remaining vertical space with a scrollable body, sticky header, and pinned pagination footer.

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,417 @@
# Execution Overlay & Debugger — Design Spec
**Sub-project:** 2 of 3 (Component → **Execution Overlay** → Page Integration)
**Scope:** Overlay real execution data onto the ProcessDiagram component from sub-project 1. Adds node status visualization, per-compound iteration stepping, a tabbed detail panel, and error navigation. Does NOT include page integration — that is sub-project 3.
---
## Problem
The ProcessDiagram from sub-project 1 shows route topology but cannot display what actually happened during an exchange's execution. Users investigating failures must cross-reference between the diagram and separate execution detail views. There is no way to see which processors were hit, which were skipped, where errors occurred, or what the message looked like at each step.
## Goal
Build an `ExecutionDiagram` wrapper component that overlays execution data onto ProcessDiagram, turning it into an "after-the-fact debugger." Users can see the execution path at a glance (green = OK, red = failed, dimmed = skipped), step through loop/split iterations independently, and inspect processor-level details (input/output body, headers, errors, timing) in a tabbed detail panel below the diagram.
---
## Decisions
| Decision | Choice | Rationale |
|----------|--------|-----------|
| Architecture | Wrapper component (`ExecutionDiagram`) composing `ProcessDiagram` | Keeps topology component pure; execution concerns isolated |
| Layout | Top/bottom IDE split (diagram top, detail panel bottom) | Left-to-right diagram needs full width; familiar IDE pattern |
| Node status | Tinted backgrounds + status badges | Green tint + checkmark for OK, red tint + ! for failed, dimmed for skipped — scannable at a glance |
| Duration display | Badge on each executed node (bottom-right) | Quick bottleneck identification without opening detail panel |
| Iteration stepping | Per-compound stepper in header bar | Independent stepping at each nesting level; contextually placed |
| Error navigation | Passive highlighting + "Jump to Error" action | Red border + ! badge on failed node; jump action drills into sub-routes if needed |
| Cross-route errors | Red border + drill-down arrow on calling node | Communicates failure exists here; arrow signals root cause is deeper |
| Detail panel tabs | Info, Headers, Input, Output, Error, Config, Timeline | Comprehensive debugging context |
| Error tab visibility | Always visible, grayed out when no error | No layout shift; consistent tab bar |
| Reusability | Component usable standalone and embedded | Immediately replaces ExchangeDetail flow view; usable elsewhere |
---
## 0. Backend Prerequisites
### Iteration fields on ProcessorNode
The `ProcessorExecution` model in `cameleer3-common` has iteration tracking fields (`loopIndex`, `loopSize`, `splitIndex`, `splitSize`, `multicastIndex`), but the server's storage layer and API response model do not surface them. The following changes are needed:
**Storage:**
- Add columns to `processor_records` table: `loop_index`, `loop_size`, `split_index`, `split_size`, `multicast_index` (all nullable integers)
- Flyway migration to add columns
- Update `ExecutionStore` to persist and read these fields
**Detail model:**
- Add fields to `ProcessorNode.java`: `loopIndex`, `loopSize`, `splitIndex`, `splitSize`, `multicastIndex`
- Update `DetailService.buildTree()` to populate them from storage
**API:**
- Regenerate `openapi.json` and `schema.d.ts` to include the new fields
### Snapshot endpoint: accept processorId
The current snapshot endpoint `GET /executions/{id}/processors/{index}/snapshot` uses a positional index into the flat processor list. This is fragile when the tree structure changes. Add an alternative parameter:
- `GET /executions/{id}/processors/by-id/{processorId}/snapshot` — fetches snapshot by processor ID
- Add corresponding `useProcessorSnapshotById(executionId, processorId)` hook on the frontend
### Diagram loading by content hash
`ExecutionDetail` includes `diagramContentHash` linking to the diagram version active during the execution. The existing `useDiagramLayout(contentHash, direction)` hook already supports loading by content hash. The `ExecutionDiagram` wrapper uses this path instead of `useDiagramByRoute(application, routeId)`.
---
## 1. ExecutionDiagram Wrapper Component
### Location
```
ui/src/components/ExecutionDiagram/
├── ExecutionDiagram.tsx # Root: top/bottom split, orchestrates overlay + detail panel
├── ExecutionDiagram.module.css # Layout styles (splitter, exchange bar, panel)
├── useExecutionOverlay.ts # Hook: maps execution data → node overlay state
├── useIterationState.ts # Hook: per-compound iteration tracking
├── ExecutionContext.tsx # React context: shares execution data + iteration state
├── DetailPanel.tsx # Bottom panel: tabs container
├── tabs/InfoTab.tsx # Processor metadata + attributes
├── tabs/HeadersTab.tsx # Input/output headers side-by-side
├── tabs/BodyTab.tsx # Shared: formatted message body (used by Input + Output)
├── tabs/ErrorTab.tsx # Exception details + stack trace
├── tabs/ConfigTab.tsx # Processor configuration (TODO: agent data)
├── tabs/TimelineTab.tsx # Gantt-style processor duration chart
├── types.ts # Overlay-specific types
└── index.ts # Public exports
```
### Props API
```typescript
interface ExecutionDiagramProps {
/** Execution to overlay — fetched externally or by executionId */
executionId: string;
/** Optional: pre-fetched execution detail (skips internal fetch) */
executionDetail?: ExecutionDetail;
/** Diagram direction */
direction?: 'LR' | 'TB';
/** Known route IDs for drill-down resolution */
knownRouteIds?: Set<string>;
/** Called when user triggers node actions (trace toggle, tap config) */
onNodeAction?: (nodeId: string, action: NodeAction) => void;
/** Active node configs (trace/tap badges) */
nodeConfigs?: Map<string, NodeConfig>;
className?: string;
}
```
### Behavior
1. Fetches `ExecutionDetail` via `useExecutionDetail(executionId)` (or uses pre-fetched prop)
2. Extracts the `diagramContentHash` from the execution to load the correct diagram version
3. Maps processor execution tree to diagram node IDs (processor IDs match diagram node IDs)
4. Passes overlay data to ProcessDiagram via new overlay props
5. Manages selected node state, detail panel content, and iteration stepping
---
## 2. ProcessDiagram Overlay Props Extension
The existing `ProcessDiagramProps` gains optional overlay props. When absent, the diagram renders in topology-only mode (sub-project 1 behavior). When present, nodes render with execution state.
```typescript
interface ProcessDiagramProps {
// ... existing props from sub-project 1 ...
/** Execution overlay: maps diagram node ID → execution state */
executionOverlay?: Map<string, NodeExecutionState>;
/** Per-compound iteration state: maps compound node ID → current iteration index */
iterationState?: Map<string, number>;
/** Called when user changes iteration on a compound stepper */
onIterationChange?: (compoundNodeId: string, iterationIndex: number) => void;
}
interface NodeExecutionState {
status: 'COMPLETED' | 'FAILED';
durationMs: number;
/** True if this node's target sub-route failed (for DIRECT/SEDA nodes) */
subRouteFailed?: boolean;
/** True if trace data (input/output body) is available */
hasTraceData?: boolean;
/** Loop/split iteration info for the compound containing this node */
iterationIndex?: number;
iterationCount?: number;
}
```
---
## 3. Node Visual States
### Executed — Completed
- Background: green tint (`#F0F9F1`)
- Border: 1.5px solid `--success` (`#3D7C47`) + 4px green left accent
- Badge: green circle with white checkmark (top-right corner, 16px diameter)
- Duration: green text bottom-right (e.g., "5ms")
### Executed — Failed
- Background: red tint (`#FDF2F0`)
- Border: 2px solid `--error` (`#C0392B`)
- Badge: red circle with white `!` (top-right corner, 16px diameter)
- Duration: red text bottom-right
- Label text turns red, subtitle shows "FAILED"
### Sub-Route Failure (DIRECT/SEDA node whose target route failed)
- Same visual as Failed (red tint, red border, red ! badge)
- Additional: drill-down arrow icon (bottom-left corner)
- "Jump to Error" action on this node auto-drills into the sub-route
### Not Executed (Skipped)
- Opacity: 35%
- No status badge, no duration badge
- Original topology styling (no tint)
### Compound Node Status
Compound nodes (CHOICE, LOOP, SPLIT, etc.) derive their status from their children:
- If any child failed → compound shows as COMPLETED (the compound itself executed) but the failed child shows individually
- The compound does not get its own status badge — only leaf processors do
- Compound background tint: subtle green if all children OK, no tint if mixed results
### RUNNING Executions
RUNNING executions are out of scope for overlay (see Non-Goals). If the `ExecutionDetail.status` is `RUNNING`, the ExecutionDiagram shows the overlay for processors that have completed so far — completed processors get green/red treatment, processors not yet reached are dimmed. No special "in-progress" visual is needed.
### Edge States
- **Traversed edge:** solid, `--success` green (`#3D7C47`), 1.5px stroke
- **Not traversed edge:** dashed, `#9CA3AF` gray, 1px stroke
---
## 4. Per-Compound Iteration Stepper
### Placement
Small control widget embedded in the compound node's header bar (right-aligned). Rendered as part of the `CompoundNode` component when overlay data includes iteration info.
### Visual
Semi-transparent background pill inside the purple/colored header:
```
LOOP [< 3 / 5 >]
```
Prev/next buttons with the current iteration and total count.
### Behavior
- Each compound (LOOP, SPLIT, MULTICAST) tracks its iteration independently via `iterationState` map
- Changing iteration updates the overlay data for all children of that compound
- Nested compounds: outer loop at iteration 2, inner split at branch 1 — independent
- CHOICE compounds: no stepper. The taken branch renders with execution state; untaken branches are dimmed
- Keyboard: left/right arrow keys step when compound is hovered
- Detail panel syncs: selecting a processor inside a loop shows that iteration's snapshot data
### Data Flow
The `useIterationState` hook maintains a `Map<compoundNodeId, currentIndex>`. When an iteration changes:
1. The hook recalculates which `ProcessorExecution` children correspond to the selected iteration (using `loopIndex`, `splitIndex`, or `multicastIndex` fields)
2. Rebuilds the `executionOverlay` map for that compound's children
3. ProcessDiagram re-renders with updated overlay
---
## 5. Exchange Summary Bar
A thin bar above the diagram showing exchange-level information:
- Exchange ID (monospace, copyable)
- Status badge (COMPLETED green, FAILED red)
- Application / route ID
- Total duration
- "Jump to Error" button (only for FAILED exchanges) — scrolls diagram to failed node, drills into sub-route if needed
---
## 6. Detail Panel
### Layout
Below the diagram, separated by a resizable splitter. Default split: 60% diagram / 40% panel. Minimum panel height: 120px. The panel can be collapsed by dragging the splitter to the bottom.
The panel has:
1. **Processor header:** selected processor name, status badge, processor ID, duration
2. **Tab bar:** Info | Headers | Input | Output | Error | Config | Timeline
3. **Tab content area:** scrollable
When no processor is selected, the panel shows exchange-level data:
- **Info tab:** exchange metadata (exchangeId, correlationId, route, application, total duration, engine level, route-level attributes)
- **Headers tab:** route-level input/output headers
- **Input tab:** route-level input body
- **Output tab:** route-level output body
- **Error tab:** route-level error (if failed)
- **Config tab:** grayed out (not applicable at exchange level)
- **Timeline tab:** Gantt chart of all processors (always available)
### Tab: Info
Grid layout showing processor metadata:
- Processor ID, Type, Status
- Start time, End time, Duration
- Endpoint URI, Resolved Endpoint URI
- Attributes section: tap-extracted attributes as pill badges
### Tab: Headers
Side-by-side layout:
- Left: Input headers (key/value table)
- Right: Output headers (key/value table)
- New/changed headers highlighted in green
Data source: `useProcessorSnapshotById(executionId, processorId)``inputHeaders`, `outputHeaders`
### Tab: Input
Formatted message body at processor entry:
- Auto-detect format (JSON, XML, plain text)
- Syntax-highlighted code block (dark theme)
- Copy button
- Byte size indicator
Data source: `useProcessorSnapshotById(executionId, processorId)``inputBody`
### Tab: Output
Same layout as Input tab, showing processor exit body.
Data source: `useProcessorSnapshotById(executionId, processorId)``outputBody`
### Tab: Error
Shown for all processors but grayed out when the selected processor has no error.
When error exists:
- Exception type (class name)
- Error message
- Root cause type + message
- Stack trace in monospace block
Data source: `ProcessorNode.errorMessage`, `ProcessorNode.errorStackTrace` from the execution detail tree
### Tab: Config
Processor configuration from the route definition. **TODO:** Requires agent-side work to capture and expose processor configuration metadata on `RouteNode`. Initially shows a placeholder indicating config data is not yet available.
### Tab: Timeline
Gantt-style horizontal bar chart showing executed processors' relative durations:
- One row per processor from the `ProcessorNode` execution tree (flattened in execution order) — only executed processors, not all diagram nodes
- Bar width proportional to duration relative to total route duration
- Green bars for completed, red for failed
- Clicking a bar selects that processor in the diagram and scrolls to it
- Duration label on the right of each row
- When inside a loop/split compound, shows the current iteration's processors
---
## 7. Data Flow
```
ExecutionDiagram
├── useExecutionDetail(executionId)
│ → ExecutionDetail { processors: ProcessorNode[], diagramContentHash, ... }
├── useExecutionOverlay(executionDetail, iterationState)
│ → Maps ProcessorNode tree → Map<diagramNodeId, NodeExecutionState>
│ → Handles iteration filtering (loopIndex, splitIndex matching)
│ → Detects sub-route failures on DIRECT/SEDA nodes
├── useIterationState()
│ → Map<compoundNodeId, currentIterationIndex>
│ → onIterationChange(compoundId, index) callback
├── ProcessDiagram
│ props: { application, routeId, executionOverlay, iterationState, onIterationChange, ... }
│ Renders nodes with overlay visual states
└── DetailPanel
├── useProcessorSnapshotById(executionId, selectedProcessorId)
│ → { inputBody, outputBody, inputHeaders, outputHeaders }
└── Tabs render from ProcessorNode + snapshot data
```
### Processor-to-Node Mapping
The `processorId` field on `ProcessorNode` is the same value as the `id` field on diagram `PositionedNode`. The agent uses diagram node IDs as processor IDs during route model extraction, so no separate mapping or `diagramNodeId` field is needed. The `useExecutionOverlay` hook builds its map by walking the `ProcessorNode` tree and keying on `processorId`, which directly matches diagram node IDs.
### Snapshot Loading
Per-processor body/header data is fetched lazily via `useProcessorSnapshotById(executionId, processorId)` when a processor is selected and the user switches to Input/Output/Headers tabs. This avoids loading all snapshot data upfront for routes with many processors. The snapshot endpoint accepts `processorId` (see Backend Prerequisites, Section 0).
---
## 8. Jump to Error
When the user clicks "Jump to Error":
1. Find the first `ProcessorNode` with `status === 'FAILED'` in the execution tree
2. If the failed processor is a DIRECT/SEDA node with `subRouteFailed: true`:
a. Drill down into the target route (same as double-click drill-down from sub-project 1)
b. Recursively find the failed processor in the sub-route's execution
3. Select the failed processor node
4. Pan/zoom the diagram to center the failed node
5. Show the Error tab in the detail panel
This handles arbitrarily deep cross-route error chains (route A calls direct:B which calls direct:C where the actual failure is).
---
## 9. Integration with ExchangeDetail Page
The `ExecutionDiagram` component replaces the existing "Flow" view tab on the `ExchangeDetail` page. The page passes `executionId` and the component handles everything internally.
```typescript
// In ExchangeDetail page
<ExecutionDiagram
executionId={executionId}
knownRouteIds={knownRouteIds}
onNodeAction={handleNodeAction}
nodeConfigs={nodeConfigs}
/>
```
The existing Gantt timeline view on ExchangeDetail can be removed or kept as an alternative view — the Timeline tab inside the detail panel provides the same functionality.
---
## Non-Goals (Sub-project 3)
- Replacing RouteFlow on the Dashboard or RouteDetail pages
- Aggregate execution heatmaps (showing hot processors across many exchanges)
- Live execution tracking (watching a RUNNING exchange in real-time)
- Diff between two executions
- Export/share execution view
---
## Verification
1. `npx tsc -p tsconfig.app.json --noEmit` passes
2. ExecutionDiagram renders on ExchangeDetail page for a known failed exchange
3. Completed nodes show green tint + checkmark + duration badge
4. Failed nodes show red tint + ! badge + red duration
5. Skipped nodes are dimmed to 35% opacity
6. Edges between executed nodes turn green; edges to skipped nodes are dashed gray
7. Loop/split compounds show iteration stepper; stepping updates child overlay
8. CHOICE compounds highlight taken branch, dim untaken branches
9. Nested loops step independently
10. Clicking a node shows its data in the detail panel
11. Detail panel tabs: Info shows metadata + attributes, Headers shows side-by-side, Input/Output show formatted body, Error shows exception + stack trace, Timeline shows Gantt chart
12. "Jump to Error" navigates to and selects the failed processor, drilling into sub-routes if needed
13. Error tab grayed out for non-failed processors
14. Config tab shows placeholder (TODO)
15. Resizable splitter between diagram and detail panel works

View File

@@ -333,3 +333,27 @@ These are explicitly out of scope for sub-project 1:
8. **Compound nodes:** Route with CHOICE renders children inside dashed container
9. **Keyboard (required):** Escape deselects, +/- zooms, 0 fits to view
10. **Direction:** `?direction=TB` renders top-to-bottom layout
---
## Implementation Notes (post-spec additions)
The following features were added during implementation beyond the original spec:
### Recursive compound nesting
EIP_WHEN, EIP_OTHERWISE, DO_CATCH, DO_FINALLY added to COMPOUND_TYPES on both backend and frontend. CompoundNode recursively renders children that are themselves compound (e.g., CHOICE → WHEN → processors).
### Edge z-ordering
Edges are distributed to their containing compound and rendered inside the compound's SVG group (after background, before children). Top-level edges stay in the main edges group. This prevents compound backgrounds from hiding edges.
### ON_COMPLETION handler sections
ON_COMPLETION nodes render as teal-tinted sections between the main flow and error handler sections. Structurally parallel to ON_EXCEPTION.
### Drill-down navigation
Double-click on DIRECT or SEDA nodes navigates into the target route's diagram. A breadcrumb bar shows the route stack and supports clicking back to any level. Escape key goes back one level. Route ID resolution handles camelCase endpoint URIs → kebab-case route IDs using the catalog's known route IDs.
### Zoom via CSS transform
The original spec proposed SVG viewBox manipulation. Implementation uses CSS `transform: translate() scale()` on the content `<g>` element instead, which is simpler and more predictable. Default zoom is 100%.
### Toolbar as HTML overlay
The original spec proposed SVG `<foreignObject>`. Implementation renders the toolbar as an absolute-positioned HTML div outside the SVG, so it maintains fixed size regardless of zoom level. Styled with design system tokens.

View File

@@ -0,0 +1,141 @@
# Navigation Redesign: Persona-Driven Layout
## Status: Implemented (2026-03-28)
## Context
The Cameleer3 UI was redesigned from page-based routing to a scope-based model with three content tabs. The Process Diagram is the major USP for the Apache Camel community.
## Core Model
**Sidebar scopes WHERE** (app -> route hierarchy), **content tabs switch WHAT** (Exchanges | Dashboard | Runtime).
## Layout Shell
```
┌──────────────────────────────────────────────────────────────────────────────┐
│ TopBar: [All > order-svc > route-a] [Cmd+K] [filters] [user] │
├───────────┬──────────────────────────────────────────────────────────────────┤
│ Sidebar │ [ Exchanges | Dashboard | Runtime ] TOTAL 443↑ ERR% 0.0%↓ │
│ ├──────────────────────────────────────────────────────────────────┤
│ Apps │ │
│ ● app-1 │ Content area (layout varies by tab + selection) │
│ ◐ app-2 │ │
│ ▾ app-3 │ │
│ route-a │ │
│ route-b │ │
│ │ │
│ ⚙ Admin │ │
└───────────┴──────────────────────────────────────────────────────────────────┘
```
## Sidebar
Simplified to **app -> route** hierarchy only. No agents (accessible via Runtime tab). Sidebar clicks act as scope filters via event interception (`display: contents` wrapper intercepts Link clicks and re-routes through current tab).
## Scope Trail
Integrated into the TopBar breadcrumb (not a separate component). Shows: `All Applications > appId > routeId`.
## Tab Bar with Inline KPIs
Content tabs (Exchanges | Dashboard | Runtime) rendered using design system `Tabs` component. Right-aligned compact KPI metrics show: Total, Err%, Avg, P99 — each with trend arrow (green=good, red=bad) computed from `useExecutionStats`. KPIs are scope-aware.
## Tab: Exchanges (Primary USP)
### Full-width mode (no exchange selected)
Exchange table (DataTable from design system) with search, sort, filter. No KPI strip (metrics are in the tab bar). Clicking a row selects the exchange via local state (not URL navigation — preserves search scope).
### Split mode (exchange selected)
Draggable 50:50 split (20%-80% range, amber highlight on drag):
```
┌──────────────────────┬───────────────────────────────────────┐
│ Exchange Table │ Exchange Header │
│ (same DataTable, │ status | attrs | app route | agent │
│ search preserved) │ Correlated Exchanges (chain) │
│ ├───────────────────────────────────────┤
│ │ Process Diagram (execution overlay) │
│ │ + minimap (bottom-right, above zoom) │
│ ├───────────────────────────────────────┤
│ │ Detail Panel (tabs) │
│ │ Info|Headers|Input|Output|Error| │
│ │ Config|Timeline|Log │
└──────────────────────┴───────────────────────────────────────┘
```
**Exchange Header** features:
- Status dot + badge + tap-collected attribute badges (consistent color via value hash)
- Clickable app name (resets to app scope), route name + GitBranch icon (resets to route scope)
- Clickable agent name + ID + Server icon (colored by agent state: green/yellow/red) -> agent pages
- Correlation chain with StatusDot per node, route name + duration, arrow connectors, total duration (far right)
- Clicking a correlated exchange switches the diagram via local state
- Clicking app/route clears the selection (returns to full-width table)
**Browser history integration:**
- Each exchange selection pushes a history entry with the selection in `location.state`
- Back/Forward restores the exact selection state (split view reappears)
- Navigating to agent details and pressing Back returns to the diagram
**Detail Panel tabs:** Info, Headers (sorted alphabetically), Input, Output, Error, Config, Timeline, **Log** (new — shows exchange logs filtered by processor when selected).
## Tab: Dashboard
Wraps existing pages: RoutesMetrics (no route scope) or RouteDetail (route scoped).
## Tab: Runtime
Wraps existing pages: AgentHealth (no agent scope) or AgentInstance (agent selected).
## Admin
Gear icon in sidebar footer. Unchanged tab-based layout (RBAC, Audit, OIDC, AppConfig, DB, OpenSearch).
## URL Structure
```
/exchanges -> Exchanges tab, no scope
/exchanges/:appId -> Exchanges tab, app-scoped
/exchanges/:appId/:routeId -> Exchanges tab, route-scoped
/dashboard -> Dashboard tab
/dashboard/:appId -> Dashboard tab, app-scoped
/dashboard/:appId/:routeId -> Dashboard tab, route-scoped
/runtime -> Runtime tab
/runtime/:appId -> Runtime tab, app-scoped
/runtime/:appId/:instanceId -> Runtime tab, agent detail
/admin/* -> Admin (unchanged)
/apps/*, /agents/* -> Legacy redirects to new paths
```
Default `/` redirects to `/exchanges`.
## Key Implementation Files
| File | Purpose |
|------|---------|
| `ui/src/hooks/useScope.ts` | URL scope parsing + navigation helpers |
| `ui/src/components/ContentTabs.tsx` | Tab bar with inline KPIs |
| `ui/src/components/TabKpis.tsx` | Compact KPI metrics (Total, Err%, Avg, P99) |
| `ui/src/components/LayoutShell.tsx` | Shell with sidebar interception, scope trail, tabs |
| `ui/src/pages/Exchanges/ExchangesPage.tsx` | Full-width / split orchestrator with history state |
| `ui/src/pages/Exchanges/ExchangeHeader.tsx` | Exchange info bar + correlation chain |
| `ui/src/pages/Dashboard/Dashboard.tsx` | Exchange table (accepts `onExchangeSelect` callback) |
| `ui/src/pages/RuntimeTab/RuntimePage.tsx` | AgentHealth / AgentInstance wrapper |
| `ui/src/pages/DashboardTab/DashboardPage.tsx` | RoutesMetrics / RouteDetail wrapper |
| `ui/src/utils/attribute-color.ts` | Deterministic badge color from value hash |
| `ui/src/components/ExecutionDiagram/tabs/LogTab.tsx` | Log tab for detail panel |
| `ui/src/router.tsx` | Tab-based routes + legacy redirects |
## What Differentiates from njams
1. **Three-tab model** — njams interleaves everything; Cameleer3 separates concerns
2. **Inline KPI metrics** — compact stats in the tab bar with trend indicators
3. **State-based selection** — exchange selection via local state, not URL navigation (preserves search)
4. **Browser history integration** — Back/Forward restores exchange selection
5. **Scope-based sidebar** — filters rather than navigates
6. **Runtime tab** — dedicated agent monitoring
7. **Draggable split** — resizable divider between table and diagram
8. **Correlation chain** — arrow-connected nodes with route names, durations, total duration
9. **Attribute badges** — consistent colors across all views via value hash

View File

@@ -0,0 +1,197 @@
# Dashboard Design Spec
## Goal
Redesign the Dashboard tab as a progressive drill-down dashboard for Apache Camel application monitoring. Follows the RED method (Rate, Errors, Duration) plus saturation (inflight exchanges). The sidebar drives scope: all applications → single application → single route. Each level answers a focused question with increasing detail.
Business/support users are the primary audience — the dashboard focuses on exchange health, throughput, error rates, and SLA compliance. Ops/infrastructure monitoring stays on the Runtime tab (agent health, JVM metrics). Power users needing custom analysis will use Grafana; this dashboard targets the 80% sweet spot.
## Architecture
The Dashboard tab renders one of three views based on the current sidebar selection:
| Sidebar state | Dashboard level | Question answered |
|---|---|---|
| No selection | Level 1: All Applications | Is my landscape healthy? Which app needs attention? |
| Application selected | Level 2: Application | How is this app performing? Which route is the problem? |
| Route selected | Level 3: Route | What's happening in this route? Where's the bottleneck? |
All levels share the same time range (controlled by the top bar's time selector) and auto-refresh behavior (LIVE mode with 30s refresh).
## Level 1: All Applications Overview
### KPI Strip (4 metrics)
| Metric | Source | Trend |
|---|---|---|
| Total throughput (msg/s) | `stats_1m_all` aggregate | vs previous period |
| Error rate (%) | failed / total | vs previous period |
| P99 latency (ms) | `approx_percentile(0.99)` | vs previous period |
| Inflight exchanges | running count | current value |
Uses the existing `useExecutionStats()` hook with no application/route filter.
### Application Health Table
Columns: App name, Status dot, Throughput (msg/s), Error Rate (%), P99 (ms), Active Routes, Sparkline (12-bucket trend).
**Status dot derivation:**
- Green: error rate < 1% AND P99 < SLA threshold (300ms)
- Yellow: error rate 1-5% OR P99 between 200-300ms
- Red: error rate > 5% OR P99 > SLA threshold
Row click navigates to that application in the sidebar (transitions to Level 2).
Data source: `useRouteMetrics()` aggregated by application. The route-level metrics are grouped by `appId` and aggregated to produce application-level rows.
### Charts (2, side-by-side)
1. **Throughput over time** — Stacked area chart, one series per application. Shows relative volume and total.
2. **Error rate over time** — Line chart, one line per application. Highlights which app is misbehaving.
Data source: `useStatsTimeseries()` with no application filter, extended to support per-app breakdown. This requires a new API parameter or a new endpoint that returns timeseries grouped by application.
### New API endpoint needed
`GET /api/v1/search/stats/timeseries/by-app` — Returns timeseries buckets grouped by application name. Query: `stats_1m_app` materialized view.
Response shape:
```json
{
"apps": {
"order-service": [ { "time": "...", "totalCount": 42, "failedCount": 1, ... } ],
"payment-gateway": [ ... ]
}
}
```
## Level 2: Single Application
### KPI Strip (4 metrics)
Same four metrics as Level 1 but scoped to the selected application. Uses `useExecutionStats({ application })`.
### Route Performance Table
Columns: Route ID, Throughput (msg/s), Success %, Avg Duration (ms), P99 Duration (ms), Error Rate (%), Sparkline.
Sortable by any column. Row click navigates to that route in the sidebar (transitions to Level 3).
Data source: `useRouteMetrics({ appId })` — already exists and returns per-route data filtered by application.
### Charts (2, side-by-side)
1. **Throughput over time** — Stacked area by route.
2. **Latency percentiles over time** — P50, P95, P99 lines with SLA threshold (300ms horizontal dashed line).
Data source: `useStatsTimeseries({ application })` for the aggregate latency chart. For per-route throughput breakdown, needs the same by-app pattern extended to by-route:
### New API endpoint needed
`GET /api/v1/search/stats/timeseries/by-route` — Returns timeseries buckets grouped by route ID, filtered by application. Query: `stats_1m_route` materialized view.
Response shape:
```json
{
"routes": {
"process-order": [ { "time": "...", "totalCount": 42, ... } ],
"validate-payment": [ ... ]
}
}
```
### Top 5 Errors
Compact table: Error Type, Route, Count, Last Seen.
Click navigates to the Exchanges tab with the error type pre-filled as a search filter. Section hidden when there are zero errors in the time window.
Data source: PostgreSQL query on `executions` table, aggregating by `error_type` column for the selected application and time range.
### New API endpoint needed
`GET /api/v1/search/errors/top` — Returns top N errors grouped by error type.
Parameters: `application` (optional), `routeId` (optional), `from`, `to`, `limit` (default 5).
Response shape:
```json
[
{ "errorType": "ConnectTimeoutException", "routeId": "validate-payment", "count": 47, "lastSeen": "2026-03-29T15:58:00Z" }
]
```
Source: PostgreSQL query on `executions` table. Group by `error_type` column (added in V10 migration). Filter to `status = 'FAILED'` within the time range. Order by count descending, limit to N.
## Level 3: Single Route
### KPI Strip (4 metrics)
Same four metrics scoped to the selected route. Uses `useExecutionStats({ application, routeId })`.
### Charts (3, in a row)
1. **Throughput over time** — Area chart, single series.
2. **Latency percentiles over time** — P50, P95, P99 lines with SLA threshold.
3. **Error rate over time** — Area chart, red-tinted.
Data source: `useStatsTimeseries({ application, routeId })` — already exists.
### Compact Process Diagram with Heatmap
Reuses the `ProcessDiagram` component but with a **latency heatmap overlay** instead of the execution overlay. Processor nodes are colored by aggregate performance:
- Color scale: green (fast) → yellow (moderate) → red (slow), computed relative to the route's own processors (not absolute thresholds). The slowest processor in the route gets the warmest color.
- Data source: `useProcessorMetrics({ routeId, appId })` — already exists. Uses `avgDurationMs` or `p99DurationMs` for the color mapping.
- No click interactions beyond visual identification. Clicking a node scrolls to its row in the processor table below.
- Compact sizing: fixed height (~250px), the diagram fits-to-view within that space.
Implementation: a new `heatmapOverlay` prop on `ProcessDiagram` (or a wrapper component) that takes a `Map<processorId, { avgMs, p99Ms }>` and colors nodes accordingly. Reuses the existing diagram layout and rendering — only the fill color logic changes.
### Processor Metrics Table
Columns: Processor ID, Type, Invocation Count, Avg Duration (ms), P99 Duration (ms), Error Rate (%).
Default sort: P99 descending (slowest processor first — highlights bottlenecks).
Data source: `useProcessorMetrics({ routeId, appId })` — already exists.
### Top 5 Errors
Same format as Level 2 but scoped to this route. Uses the same `top-errors` endpoint with `routeId` parameter.
## Data Requirements Summary
### Existing endpoints (no backend changes)
| Endpoint | Used at |
|---|---|
| `GET /search/stats` | All levels (KPI strip) |
| `GET /search/stats/timeseries` | Level 2, Level 3 (charts) |
| `GET /routes/metrics` | Level 1 (app table, aggregated), Level 2 (route table) |
| `GET /routes/metrics/processors` | Level 3 (processor table + heatmap) |
### New endpoints needed
| Endpoint | Used at | Source |
|---|---|---|
| `GET /search/stats/timeseries/by-app` | Level 1 (charts) | `stats_1m_app` view |
| `GET /search/stats/timeseries/by-route` | Level 2 (throughput chart) | `stats_1m_route` view |
| `GET /search/errors/top` | Level 2, Level 3 (top errors) | `executions` table |
### Existing frontend components reused
- `KpiStrip` / `TabKpis` — KPI display with trends
- `DataTable` — sortable tables
- `AreaChart`, `LineChart` — time-series charts
- `Sparkline` — compact trend in table cells
- `StatusDot` — health indicators
- `ProcessDiagram` — route visualization (extended with heatmap)
## Scope Exclusions
- No user-customizable panels or drag-and-drop layout (power users use Grafana)
- No JVM/infrastructure metrics on Dashboard tab (that's Runtime tab)
- No alerting or threshold configuration (out of scope)
- No comparison mode (e.g., "this week vs last week" side-by-side)
- No export/PDF functionality

1
openapi.json Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 192 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 232 KiB

Some files were not shown because too many files have changed in this diff Show More