310 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
hsiegeln
eb9c20e734 feat: drill-down into sub-routes with breadcrumb navigation
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 43s
Double-click a DIRECT or SEDA node to navigate into that route's
diagram. Breadcrumbs show the route stack and allow clicking back
to any level. Escape key goes back one level.

Route ID resolution handles camelCase endpoint URIs mapping to
kebab-case route IDs (e.g. direct:callGetProduct → call-get-product)
using the catalog's known route IDs.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 16:58:35 +01:00
hsiegeln
f6220a9f89 feat: support ON_COMPLETION handler sections in diagram
Add ON_COMPLETION to backend COMPOUND_TYPES and frontend rendering.
Completion handlers render as teal-tinted sections between the main
flow and error handlers, structurally parallel to onException.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 16:45:10 +01:00
hsiegeln
9b7626f6ff fix: diagram rendering improvements
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 37s
- Recursive compound rendering: CompoundNode checks if children are
  themselves compound types (WHEN inside CHOICE) and renders them
  recursively. Added EIP_WHEN, EIP_OTHERWISE, DO_CATCH, DO_FINALLY
  to frontend COMPOUND_TYPES.
- Edge z-ordering: edges are distributed to their containing compound
  and rendered after the background rect, so they're not hidden behind
  compound containers.
- Error section sizing: normalize error handler node coordinates to
  start at (0,0), compute red tint background height from actual
  content with symmetric padding for vertical centering.
- Toolbar as HTML overlay: moved from SVG foreignObject to absolute-
  positioned HTML div so it stays fixed size at any zoom level. Uses
  design system tokens for consistent styling.
- Zoom: replaced viewBox approach with CSS transform on content group.
  Default zoom is 100% anchored top-left. Fit-to-view still available
  via button.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 16:33:24 +01:00
hsiegeln
20d1182259 fix: recursive compound nesting, fixed node width, zoom crash
All checks were successful
CI / cleanup-branch (push) Has been skipped
CI / build (push) Successful in 1m0s
CI / docker (push) Successful in 52s
CI / deploy-feature (push) Has been skipped
CI / deploy (push) Successful in 36s
ELK renderer:
- Add EIP_WHEN, EIP_OTHERWISE, DO_CATCH, DO_FINALLY to COMPOUND_TYPES
  so branch body processors nest inside their containers
- Rewrite node creation and result extraction as recursive methods
  to support compound-inside-compound (CHOICE → WHEN → processors)
- Use fixed NODE_WIDTH=160 for leaf nodes instead of variable width

Frontend:
- Fix mousewheel crash: capture getBoundingClientRect() before
  setState updater (React nulls currentTarget after handler returns)
- Anchor fitToView to top-left instead of centering

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 14:26:35 +01:00
hsiegeln
afcb7d3175 fix: DevDiagram page uses time range and correct catalog shape
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 37s
The dev diagram page was calling useRouteCatalog() without time range
params (returned empty) and parsing the wrong response shape (expected
flat {application, routeId} but catalog returns {appId, routes[]}).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 14:05:32 +01:00
hsiegeln
ac32396a57 feat: add interactive ProcessDiagram SVG component (sub-project 1/3)
All checks were successful
CI / cleanup-branch (push) Has been skipped
CI / build (push) Successful in 1m0s
CI / docker (push) Successful in 56s
CI / deploy-feature (push) Has been skipped
CI / deploy (push) Successful in 38s
New interactive route diagram component with SVG rendering using
server-computed ELK layout coordinates. TIBCO BW5-inspired top-bar
card node style with zoom/pan, hover toolbars, config badges, and
error handler sections below the main flow.

Backend: add direction query parameter (LR/TB) to diagram render
endpoints, defaulting to left-to-right layout.

Frontend: 14-file ProcessDiagram component in ui/src/components/
with DiagramNode, CompoundNode, DiagramEdge, ConfigBadge, NodeToolbar,
ErrorSection, ZoomControls, and supporting hooks. Dev test page at
/dev/diagram for validation.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 13:55:29 +01:00
hsiegeln
78e12f5cf9 fix: separate onException/errorHandler into distinct RouteFlow segments
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 37s
ON_EXCEPTION and ERROR_HANDLER nodes are now treated as compound containers
in the ELK diagram renderer, nesting their children. The frontend
diagram-mapping builds separate FlowSegments for each error handler,
displayed as distinct sections in the RouteFlow component.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 09:15:06 +01:00
hsiegeln
62709ce80b feat: include tap attributes in cmd-K full-text search
All checks were successful
CI / cleanup-branch (push) Has been skipped
CI / build (push) Successful in 1m4s
CI / docker (push) Successful in 1m13s
CI / deploy-feature (push) Has been skipped
CI / deploy (push) Successful in 36s
Add attributes_text flattened field to OpenSearch indexing for both
execution and processor levels. Include in full-text search queries,
wildcard matching, and highlighting. Merge processor-level attributes
into ExecutionSummary. Add 'attribute' category to CommandPalette
(design-system 0.1.17) with per-key-value results in the search UI.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 08:13:58 +01:00
hsiegeln
ea88042ef5 fix: exclude search endpoint from audit log
All checks were successful
CI / cleanup-branch (push) Has been skipped
CI / build (push) Successful in 1m1s
CI / docker (push) Successful in 37s
CI / deploy-feature (push) Has been skipped
CI / deploy (push) Successful in 28s
POST /api/v1/search/executions is a read-only query using POST for the
request body. Skip it in AuditInterceptor to avoid flooding the audit
log with search operations.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 23:55:24 +01:00
hsiegeln
cde79bd172 fix: remove stale diagramNodeId from test ProcessorRecord constructors
All checks were successful
CI / cleanup-branch (push) Has been skipped
CI / build (push) Successful in 1m0s
CI / docker (push) Successful in 1m16s
CI / deploy-feature (push) Has been skipped
CI / deploy (push) Successful in 27s
TreeReconstructionTest and PostgresExecutionStoreIT still passed the
removed diagramNodeId parameter. Missed by mvn compile (main only);
caught by mvn verify (test compilation).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 23:40:13 +01:00
hsiegeln
a2a8e4ae3f feat: rename logForwardingLevel to applicationLogLevel, add agentLogLevel
Some checks failed
CI / cleanup-branch (push) Has been skipped
CI / build (push) Failing after 39s
CI / docker (push) Has been skipped
CI / deploy (push) Has been skipped
CI / deploy-feature (push) Has been skipped
Align with cameleer3-common rename: logForwardingLevel → applicationLogLevel
(root logger) and new agentLogLevel (com.cameleer3 logger). Both fields
are on ApplicationConfig, pushed via config-update. UI shows "App Log Level"
and "Agent Log Level" on AppConfig slide-in, AgentHealth config bar, and
AppConfigDetailPage.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 23:36:31 +01:00
hsiegeln
6e187ccb48 feat: native TRACE log level with design system 0.1.16
Some checks failed
CI / cleanup-branch (push) Has been skipped
CI / build (push) Failing after 35s
CI / docker (push) Has been skipped
CI / deploy (push) Has been skipped
CI / deploy-feature (push) Has been skipped
Map TRACE to its own 'trace' level instead of grouping with DEBUG,
now that the design system LogViewer supports it natively.
Bump @cameleer/design-system to 0.1.16.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 23:07:42 +01:00
hsiegeln
862a27b0b8 feat: add TRACE log level support across UI
Some checks failed
CI / cleanup-branch (push) Has been skipped
CI / build (push) Failing after 34s
CI / docker (push) Has been skipped
CI / deploy (push) Has been skipped
CI / deploy-feature (push) Has been skipped
Add TRACE option to log forwarding level dropdowns (AppConfig,
AgentHealth), badge color mapping, and log filter ButtonGroups
on all pages that display application logs.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 23:03:15 +01:00
hsiegeln
d6c1f2c25b refactor: derive processor-route mapping from diagrams instead of executions
Some checks failed
CI / cleanup-branch (push) Has been skipped
CI / build (push) Failing after 37s
CI / docker (push) Has been skipped
CI / deploy (push) Has been skipped
CI / deploy-feature (push) Has been skipped
Store application_name in route_diagrams at ingestion time (V7 migration),
resolve from agent registry same as ExecutionController. Move
findProcessorRouteMapping from ExecutionStore to DiagramStore using a
JSONB query that extracts node IDs directly from stored RouteGraph
definitions. This makes the mapping available as soon as diagrams are
sent, before any executions are recorded.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 23:00:10 +01:00
hsiegeln
100b780b47 refactor: remove diagramNodeId indirection, use processorId directly
Some checks failed
CI / cleanup-branch (push) Has been skipped
CI / build (push) Failing after 37s
CI / docker (push) Has been skipped
CI / deploy (push) Has been skipped
CI / deploy-feature (push) Has been skipped
Agent now uses Camel processorId as RouteNode.id, eliminating the
nodeId mapping layer. Drop diagram_node_id column (V6 migration),
remove from ProcessorRecord/ProcessorNode/IngestionService/DetailService,
add /processor-routes endpoint for processorId→routeId lookup,
simplify frontend diagram-mapping and ExchangeDetail overlays,
replace N diagram fetches in AppConfigPage with single hook.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 22:44:07 +01:00
hsiegeln
bd63a8ce95 feat: App Config slide-in with Route column, clickable taps, and edit toolbar
All checks were successful
CI / cleanup-branch (push) Has been skipped
CI / build (push) Successful in 1m4s
CI / docker (push) Successful in 1m19s
CI / deploy-feature (push) Has been skipped
CI / deploy (push) Successful in 27s
- Add Route column to Traces & Taps table (diagram-based mapping, pending backend fix)
- Make tap badges clickable to navigate to route's Taps tab
- Add edit/save/cancel toolbar with design system Button components
- Move Sampling Rate to last position in settings grid
- Support ?tab= URL param on RouteDetail for direct tab navigation
- Bump @cameleer/design-system to 0.1.15 (DetailPanel overlay + backdrop)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 22:26:28 +01:00
hsiegeln
ef9ec6069f fix: improve App Config slide-in panel layout
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 26s
- Narrowed panel from 640px to 520px so main table columns stay visible
- Settings grid uses CSS grid (3 columns) for proper wrapping
- Removed unused PanelActions component that caused white footer bar

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 21:49:03 +01:00
hsiegeln
bf84f1814f feat: convert App Config detail to slide-in DetailPanel
All checks were successful
CI / cleanup-branch (push) Has been skipped
CI / build (push) Successful in 1m20s
CI / docker (push) Successful in 1m24s
CI / deploy-feature (push) Has been skipped
CI / deploy (push) Successful in 28s
Replaces the separate AppConfigDetailPage route with a 640px-wide
DetailPanel that slides in when clicking a row on the App Config
overview table. All editing functionality (settings, traces & taps,
route recording) is preserved inside the panel.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 21:44:30 +01:00
hsiegeln
00efaf0ca0 chore: bump @cameleer/design-system to 0.1.14
All checks were successful
CI / cleanup-branch (push) Has been skipped
CI / build (push) Successful in 1m0s
CI / docker (push) Successful in 1m14s
CI / deploy-feature (push) Has been skipped
CI / deploy (push) Successful in 26s
Picks up LogViewer background fix (removes --bg-inset for consistent
card backgrounds).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 21:35:11 +01:00
hsiegeln
900b6f45c5 fix: use pencil and trash icons for tap row actions
All checks were successful
CI / cleanup-branch (push) Has been skipped
CI / build (push) Successful in 1m3s
CI / docker (push) Successful in 1m25s
CI / deploy-feature (push) Has been skipped
CI / deploy (push) Successful in 27s
Replaces text "Edit"/"Del" buttons with pencil and trash can icon
buttons matching the style used elsewhere in the UI.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 21:32:05 +01:00
hsiegeln
dd6ea7563f feat: use Toggle switch for metrics setting on AgentHealth config bar
Some checks failed
CI / cleanup-branch (push) Has been skipped
CI / build (push) Successful in 58s
CI / deploy (push) Has been cancelled
CI / deploy-feature (push) Has been cancelled
CI / docker (push) Has been cancelled
Replaces the plain checkbox with the design system Toggle component
for consistency with the recording toggle on RouteDetail and
AppConfigDetailPage.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 21:30:35 +01:00
hsiegeln
57bb84a2df fix: align edit and save/cancel buttons after badges on AgentHealth
Some checks failed
CI / cleanup-branch (push) Has been skipped
CI / build (push) Successful in 56s
CI / docker (push) Successful in 54s
CI / deploy-feature (push) Has been skipped
CI / deploy (push) Has been cancelled
Moved edit pencil and save/cancel actions to sit right after the last
badge field instead of at the start or far right of the config bar.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 21:28:30 +01:00
hsiegeln
a0fbf785c3 fix: move config edit button to right side of badges on AgentHealth
Some checks failed
CI / cleanup-branch (push) Has been skipped
CI / build (push) Successful in 56s
CI / deploy (push) Has been cancelled
CI / deploy-feature (push) Has been cancelled
CI / docker (push) Has been cancelled
Moved the pencil edit button after the badge fields and added
margin-left: auto to push it to the far right of the config bar.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 21:27:01 +01:00
hsiegeln
91e51d4f6a feat: show configured taps count on Admin App Config overview
All checks were successful
CI / cleanup-branch (push) Has been skipped
CI / build (push) Successful in 54s
CI / docker (push) Successful in 55s
CI / deploy-feature (push) Has been skipped
CI / deploy (push) Successful in 37s
New Taps column shows enabled/total count as a badge (e.g. "2/3")
next to the existing Traced column.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 21:22:59 +01:00
hsiegeln
b52d588fc5 feat: add tooltips to tap attribute type selector buttons
All checks were successful
CI / cleanup-branch (push) Has been skipped
CI / build (push) Successful in 55s
CI / docker (push) Successful in 50s
CI / deploy-feature (push) Has been skipped
CI / deploy (push) Successful in 36s
Each type option now shows a descriptive tooltip on hover explaining
its purpose: Business Object (key identifiers), Correlation (cross-route
linking), Event (business events), Custom (general purpose).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 19:47:39 +01:00
hsiegeln
23b23bbb66 fix: replace crypto.randomUUID with fallback for non-HTTPS contexts
Some checks failed
CI / cleanup-branch (push) Has been skipped
CI / build (push) Successful in 52s
CI / deploy (push) Has been cancelled
CI / deploy-feature (push) Has been cancelled
CI / docker (push) Has been cancelled
crypto.randomUUID() requires a secure context (HTTPS). Since the server
may be accessed via HTTP, use a timestamp + random string ID instead.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 19:46:32 +01:00
hsiegeln
82b47f4364 fix: use design system status tokens for test expression result alerts
All checks were successful
CI / cleanup-branch (push) Has been skipped
CI / build (push) Successful in 55s
CI / docker (push) Successful in 47s
CI / deploy-feature (push) Has been skipped
CI / deploy (push) Successful in 36s
Replaces hardcoded dark-theme hex fallbacks with proper tokens from
tokens.css: --success-bg/--success-border/--success for success and
--error-bg/--error-border/--error for errors. Works in both themes.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 19:38:24 +01:00
hsiegeln
e4b2dd2604 fix: use design system tokens for tap type selector active state
Some checks failed
CI / cleanup-branch (push) Has been skipped
CI / build (push) Successful in 56s
CI / deploy (push) Has been cancelled
CI / deploy-feature (push) Has been cancelled
CI / docker (push) Has been cancelled
The active type option was invisible because --accent-primary doesn't
exist in the design system. Now uses --amber-bg/--amber-deep/--amber
from tokens.css for a clearly visible selected state matching the
brand accent palette.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 19:37:12 +01:00
hsiegeln
3b31e69ae4 chore: regenerate openapi.json and schema.d.ts from live server
All checks were successful
CI / cleanup-branch (push) Has been skipped
CI / build (push) Successful in 54s
CI / docker (push) Successful in 48s
CI / deploy-feature (push) Has been skipped
CI / deploy (push) Successful in 36s
Updated types now include attributes on ExecutionDetail, ProcessorNode,
and ExecutionSummary from the actual API. Removed stale detail.children
fallback that no longer exists in the schema.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 19:22:55 +01:00
hsiegeln
499fd7f8e8 fix: accept ISO datetime for audit log from/to parameters
All checks were successful
CI / cleanup-branch (push) Has been skipped
CI / build (push) Successful in 54s
CI / docker (push) Successful in 37s
CI / deploy-feature (push) Has been skipped
CI / deploy (push) Successful in 36s
The frontend sends full ISO timestamps (e.g. 2026-03-19T17:55:29Z) but
the controller expected LocalDate (yyyy-MM-dd). This caused null parsing,
which threw NullPointerException in the repository WHERE clause. Changed
to accept Instant directly with sensible defaults (last 7 days).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 19:07:09 +01:00
hsiegeln
1080c76e99 feat: wire attributes from RouteExecution/ProcessorExecution into storage
All checks were successful
CI / cleanup-branch (push) Has been skipped
CI / build (push) Successful in 54s
CI / docker (push) Successful in 36s
CI / deploy-feature (push) Has been skipped
CI / deploy (push) Successful in 36s
Replaces null placeholders with actual getAttributes() calls now that
cameleer3-common SNAPSHOT is resolved with attributes support.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 19:03:18 +01:00
hsiegeln
7f58bca0e6 chore: update IngestionService TODO comments for attributes wiring
All checks were successful
CI / cleanup-branch (push) Has been skipped
CI / build (push) Successful in 59s
CI / docker (push) Successful in 50s
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-26 18:59:17 +01:00
hsiegeln
c087e4af08 fix: add missing attributes parameter to test record constructors
Tests were not updated when attributes field was added to ExecutionRecord,
ProcessorRecord, ProcessorDoc, and ExecutionDocument records.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 18:58:44 +01:00
hsiegeln
387ed44989 fix: add missing attributes parameter to test record constructors
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-26 18:58:32 +01:00
hsiegeln
64b677696e feat(ui): restructure AppConfigDetailPage into 3 sections
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
Merge Logging + Observability into unified "Settings" section with
flex-wrap badge grid including new compressSuccess toggle. Merge
Traced Processors with Taps into "Traces & Taps" section showing
capture mode and tap badges per processor. Add "Route Recording"
section with per-route toggles sourced from route catalog. All new
fields (compressSuccess, routeRecording) included in form state
and save payload.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 18:48:14 +01:00
hsiegeln
78813ea15f feat(ui): add taps DataTable, CRUD modal with test expression to RouteDetail
- Replace taps tab placeholder with full DataTable showing all route taps
- Add columns: attribute, processor, expression, language, target, type, enabled toggle, actions
- Add tap modal with form fields: attribute name, processor select, language, target, expression, type selector
- Implement inline enable/disable toggle per tap row
- Add ConfirmDialog for tap deletion
- Add test expression section with Recent Exchange and Custom Payload tabs
- Add save/edit/delete tap operations via application config update
- Add all supporting CSS module classes (no inline styles)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 18:44:36 +01:00
hsiegeln
807e191397 feat(ui): add recording toggle, active taps KPI, and taps tab to RouteDetail
- Add Toggle for route recording on/off in the route header
- Fetch application config to determine recording state and route taps
- Add Active Taps KPI card showing enabled/total tap counts
- Add Taps tab to the tabbed section with placeholder content

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 18:44:06 +01:00
hsiegeln
47ff122c48 feat: add Attributes column to Dashboard exchanges table
Shows up to 2 attribute badges (color="auto") per row with a +N overflow
indicator; empty rows render a muted dash. Uses CSS module classes only.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-26 18:36:53 +01:00
hsiegeln
eb796f531f feat(ui): add replay modal to ExchangeDetail page
Add a Replay button in the exchange header that opens a modal allowing
users to re-send the exchange to a live agent. The modal pre-populates
headers and body from the original exchange input, provides an agent
selector filtered to live agents for the application, and supports
editable header key-value rows with add/remove.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 18:35:00 +01:00
hsiegeln
a3706cf7c2 feat(ui): display business attributes on ExchangeDetail page
Show route-level attributes as Badge strips in the exchange header
card, and per-processor attributes above the message IN/OUT panels.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 18:33:16 +01:00
hsiegeln
2b1d49c032 feat: add TapDefinition, extend ApplicationConfig, and add API hooks
- Add TapDefinition interface for tap configuration
- Extend ApplicationConfig with taps, tapVersion, routeRecording, compressSuccess
- Add useTestExpression mutation hook (manual fetch to new endpoint)
- Add useReplayExchange mutation hook (uses api client, targets single agent)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-26 18:29:52 +01:00
hsiegeln
ae1ee38441 feat: add attributes fields to schema.d.ts types
Add optional `attributes?: Record<string, string>` to ExecutionSummary,
ExecutionDetail, and ProcessorNode in the manually-maintained OpenAPI
schema to reflect the new backend attributes support.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-26 18:29:47 +01:00
hsiegeln
d6d96aad07 feat: add TEST_EXPRESSION command with request-reply infrastructure
Adds CompletableFuture-based request-reply mechanism for commands that
need synchronous results. CommandReply record in core, pendingReplies
map in AgentRegistryService, test-expression endpoint on config controller
with 5s timeout. CommandAckRequest extended with optional data field.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 18:27:59 +01:00
hsiegeln
2d6cc4c634 feat(search): deserialize and surface attributes in detail service and OpenSearch indexing (Task 4)
DetailService deserializes attributes JSON from ExecutionRecord/ProcessorRecord and
passes them to ExecutionDetail and ProcessorNode constructors. ExecutionDocument and
ProcessorDoc carry attributes as a JSON string. SearchIndexer passes attributes when
building documents. OpenSearchIndex includes attributes in indexed maps and
deserializes them when constructing ExecutionSummary from search hits.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-26 18:23:47 +01:00
hsiegeln
ca5250c134 feat(ingestion): wire attributes through ingestion pipeline into PostgreSQL (Task 3)
IngestionService passes attributes (currently null, pending cameleer3-common update)
to ExecutionRecord and ProcessorRecord. PostgresExecutionStore includes the
attributes column in INSERT and ON CONFLICT UPDATE (with COALESCE), and reads
it back in both row mappers.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-26 18:23:38 +01:00
hsiegeln
64f797bd96 feat(core): add attributes field to storage records and detail/summary models (Task 2)
Adds Map<String,String> attributes to ExecutionRecord, ProcessorRecord,
ExecutionDetail, ProcessorNode, and ExecutionSummary. ExecutionStore records
carry attributes as a JSON string; detail/summary models carry deserialized maps.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-26 18:23:32 +01:00
hsiegeln
f08461cf35 feat(db): add attributes JSONB columns to executions and processor_executions (Task 1)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-26 18:23:26 +01:00
hsiegeln
2b5d803a60 docs: add implementation plan for taps, attributes, replay UI features
14-task plan covering: database migration, attributes pipeline, test-expression
command with request-reply, OpenAPI regeneration, frontend types/hooks,
ExchangeDetail attributes + replay modal, Dashboard attributes column,
RouteDetail recording toggle + taps tab + tap CRUD modal, and
AppConfigDetailPage restructure.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 18:13:58 +01:00
hsiegeln
e3902cd85f docs: add UI design spec for taps, attributes, replay, recording & compression
Covers all 5 new agent features: tap management on RouteDetail, business
attributes display on ExchangeDetail/Dashboard, enhanced replay with
editable payload, per-route recording toggles, and success compression.
Includes backend prerequisites, RBAC matrix, and TypeScript interfaces.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 17:48:20 +01:00
hsiegeln
25ca8d5132 feat: show log indices on OpenSearch admin page
All checks were successful
CI / cleanup-branch (push) Has been skipped
CI / build (push) Successful in 54s
CI / docker (push) Successful in 47s
CI / deploy-feature (push) Has been skipped
CI / deploy (push) Successful in 35s
Add prefix query parameter to /admin/opensearch/indices endpoint so
the UI can fetch execution and log indices separately. OpenSearch admin
page now shows two card sections: Execution Indices and Log Indices,
each with doc count and size summary. Page restyled with CSS module
replacing inline styles. Delete endpoint also allows log index deletion.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 16:47:44 +01:00
hsiegeln
0d94132c98 feat: SOC2 audit log completeness — hybrid interceptor + explicit calls
All checks were successful
CI / cleanup-branch (push) Has been skipped
CI / build (push) Successful in 54s
CI / docker (push) Successful in 51s
CI / deploy-feature (push) Has been skipped
CI / deploy (push) Successful in 37s
Add AuditInterceptor as a safety net that auto-audits any POST/PUT/DELETE
without an explicit audit call (excludes data ingestion + heartbeat).
AuditService sets a request attribute so the interceptor skips when
explicit logging already happened.

New explicit audit calls:
- ApplicationConfigController: view/update app config
- AgentCommandController: send/broadcast commands (AGENT category)
- AgentRegistrationController: agent register + token refresh
- UiAuthController: UI token refresh
- OidcAuthController: OIDC callback failure
- AuditLogController: view audit log (sensitive read)
- UserAdminController: view users (sensitive read)
- OidcConfigAdminController: view OIDC config (sensitive read)

New AuditCategory.AGENT added. Frontend audit log filter updated.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 16:41:10 +01:00
hsiegeln
0e6de69cd9 feat: add App Config detail page with view/edit mode
All checks were successful
CI / cleanup-branch (push) Has been skipped
CI / build (push) Successful in 53s
CI / docker (push) Successful in 52s
CI / deploy-feature (push) Has been skipped
CI / deploy (push) Successful in 38s
Click a row in the admin App Config table to navigate to a dedicated
detail page at /admin/appconfig/:appId. Shows all config fields as
badges in view mode; pencil toggles to edit mode with dropdowns.

Traced processors are now editable (capture mode dropdown + remove
button per processor). Sections and header use card styling for
visual contrast. OidcConfigPage gets the same card treatment.

List page simplified to read-only badge overview with row click
navigation.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 16:15:27 +01:00
hsiegeln
e53274bcb9 fix: LogViewer and EventFeed scroll to top on load
All checks were successful
CI / cleanup-branch (push) Has been skipped
CI / build (push) Successful in 56s
CI / docker (push) Successful in 1m9s
CI / deploy-feature (push) Has been skipped
CI / deploy (push) Successful in 35s
Update design system to v0.1.13 where both components scroll to the
top (newest entries) instead of the bottom, matching the descending
sort order used across the UI.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 15:54:56 +01:00
hsiegeln
4433b26bf8 fix: move pencil/save buttons to start of config bar for consistency
All checks were successful
CI / cleanup-branch (push) Has been skipped
CI / build (push) Successful in 57s
CI / docker (push) Successful in 50s
CI / deploy-feature (push) Has been skipped
CI / deploy (push) Successful in 35s
Pencil icon and Save/Cancel buttons now appear at the left side of
the AgentHealth config bar, matching the admin overview table where
the edit column is at the start of each row.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 15:38:36 +01:00
hsiegeln
74fa08f41f fix: visible Save/Cancel buttons on AgentHealth config edit mode
All checks were successful
CI / cleanup-branch (push) Has been skipped
CI / build (push) Successful in 56s
CI / docker (push) Successful in 52s
CI / deploy-feature (push) Has been skipped
CI / deploy (push) Successful in 37s
Replace subtle Unicode checkmark/X with proper labeled buttons styled
as primary (Save) and secondary (Cancel) for better visibility.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 13:20:11 +01:00
hsiegeln
4b66d78cf4 refactor: config settings shown as badges with pencil-to-edit
All checks were successful
CI / cleanup-branch (push) Has been skipped
CI / build (push) Successful in 56s
CI / docker (push) Successful in 47s
CI / deploy-feature (push) Has been skipped
CI / deploy (push) Successful in 36s
Settings (log level, engine level, payload capture, metrics) now
display as color-coded badges by default. Clicking the pencil icon
enters edit mode where badges become dropdowns. Save (checkmark)
persists changes and reverts to badge view; cancel discards changes.

Applied consistently on both the admin App Config page and the
AgentHealth config bar.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 13:12:56 +01:00
hsiegeln
b1c2950b1e fix: add id field to AppConfigPage DataTable rows
All checks were successful
CI / cleanup-branch (push) Has been skipped
CI / build (push) Successful in 2m51s
CI / docker (push) Successful in 1m9s
CI / deploy-feature (push) Has been skipped
CI / deploy (push) Successful in 35s
DataTable requires rows with an { id: string } constraint. Map
ApplicationConfig to ConfigRow adding id from the application field.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 12:55:19 +01:00
hsiegeln
b0484459a2 feat: add application config overview and inline editing
Some checks failed
CI / cleanup-branch (push) Has been skipped
CI / build (push) Failing after 22s
CI / docker (push) Has been skipped
CI / deploy (push) Has been skipped
CI / deploy-feature (push) Has been skipped
Add admin page at /admin/appconfig with a DataTable showing all
application configurations. Inline dropdowns allow editing log level,
engine level, payload capture mode, and metrics toggle directly from
the table. Changes push to agents via SSE immediately.

Also adds a config bar on the AgentHealth page (/agents/:appId) for
per-application config management with the same 4 settings.

Backend: GET /api/v1/config list endpoint, findAll() on repository,
sensible defaults for logForwardingLevel/engineLevel/payloadCaptureMode.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 12:51:07 +01:00
hsiegeln
056a6f0ff5 feat: sidebar exchange counts respect selected time range
All checks were successful
CI / cleanup-branch (push) Has been skipped
CI / build (push) Successful in 2m47s
CI / docker (push) Successful in 48s
CI / deploy-feature (push) Has been skipped
CI / deploy (push) Successful in 36s
The /routes/catalog endpoint now accepts optional from/to query
parameters instead of hardcoding a 24h window. The UI passes the
global filter time range so sidebar counts match what the user sees.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 12:21:10 +01:00
hsiegeln
f4bf38fcba feat: add inspect column to agent instance data table
All checks were successful
CI / build (push) Successful in 58s
CI / cleanup-branch (push) Has been skipped
CI / docker (push) Successful in 58s
CI / deploy (push) Successful in 35s
CI / deploy-feature (push) Has been skipped
Add a dedicated inspect button column (↗) to navigate to the agent
instance page, consistent with the exchange inspect pattern on the
Dashboard. Row click still opens the detail slide-in panel.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 12:04:06 +01:00
hsiegeln
15632a2170 fix: show full exchange ID in breadcrumb
All checks were successful
CI / build (push) Successful in 53s
CI / cleanup-branch (push) Has been skipped
CI / docker (push) Successful in 47s
CI / deploy (push) Successful in 35s
CI / deploy-feature (push) Has been skipped
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 11:49:41 +01:00
hsiegeln
479b67cd2d refactor: consolidate breadcrumbs to single TopBar instance
All checks were successful
CI / build (push) Successful in 1m1s
CI / cleanup-branch (push) Has been skipped
CI / docker (push) Successful in 1m11s
CI / deploy (push) Successful in 35s
CI / deploy-feature (push) Has been skipped
Remove duplicate in-page breadcrumbs (ExchangeDetail, AgentHealth scope
trail) and improve the global TopBar breadcrumb with semantic labels and
a context-based override for pages with richer navigation data.

- Add BreadcrumbProvider from design system v0.1.12
- LayoutShell: label map prettifies URL segments (apps→Applications, etc.)
- ExchangeDetail: uses useBreadcrumb() to set semantic trail via context
- AgentHealth: remove scope trail, keep live-count badge standalone

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 11:40:37 +01:00
hsiegeln
bde0459416 fix: prevent log viewer flicker on ExchangeDetail page
All checks were successful
CI / build (push) Successful in 1m0s
CI / cleanup-branch (push) Has been skipped
CI / docker (push) Successful in 1m12s
CI / deploy (push) Successful in 35s
CI / deploy-feature (push) Has been skipped
Skip global time range in the logs query key when filtering by
exchangeId (exchange logs are historical, the sliding time window is
irrelevant). Add placeholderData to keep previous results visible
during query key transitions on other pages.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 11:03:38 +01:00
hsiegeln
a01712e68c fix: use .keyword suffix on both exchangeId term queries
All checks were successful
CI / build (push) Successful in 1m1s
CI / cleanup-branch (push) Has been skipped
CI / docker (push) Successful in 41s
CI / deploy (push) Successful in 36s
CI / deploy-feature (push) Has been skipped
Defensive: use .keyword on the top-level exchangeId field too, in
case indices were created before the explicit keyword mapping was
added to the template.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 10:45:59 +01:00
hsiegeln
9aa78f681d fix: use .keyword suffix for MDC exchangeId term query
Some checks failed
CI / docker (push) Has been cancelled
CI / deploy (push) Has been cancelled
CI / deploy-feature (push) Has been cancelled
CI / cleanup-branch (push) Has been cancelled
CI / build (push) Has been cancelled
Dynamically mapped string fields in OpenSearch are multi-field
(text + keyword). Term queries require the .keyword sub-field for
exact matching.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 10:45:14 +01:00
hsiegeln
befefe457f fix: query both top-level and MDC exchangeId for log search
All checks were successful
CI / build (push) Successful in 1m1s
CI / cleanup-branch (push) Has been skipped
CI / docker (push) Successful in 49s
CI / deploy (push) Successful in 39s
CI / deploy-feature (push) Has been skipped
Existing log records only have exchangeId inside the mdc object, not
as a top-level indexed field. Use a bool should clause to match on
either exchangeId (new records) or mdc.camel.exchangeId (old records).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 10:40:42 +01:00
hsiegeln
ea665ff411 feat: exchange-level log viewer on ExchangeDetail page
All checks were successful
CI / build (push) Successful in 1m0s
CI / cleanup-branch (push) Has been skipped
CI / docker (push) Successful in 49s
CI / deploy (push) Successful in 37s
CI / deploy-feature (push) Has been skipped
Index exchangeId from Camel MDC (camel.exchangeId) as a top-level
keyword field in OpenSearch log indices. Add exchangeId filter to
the log query API and frontend hook. Show a LogViewer on the
ExchangeDetail page filtered to that exchange's logs, with search
input and level filter pills.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 10:26:30 +01:00
hsiegeln
f9bd492191 chore: update design system to v0.1.11 (live time range fix)
All checks were successful
CI / build (push) Successful in 56s
CI / cleanup-branch (push) Has been skipped
CI / docker (push) Successful in 1m9s
CI / deploy (push) Successful in 39s
CI / deploy-feature (push) Has been skipped
The GlobalFilterProvider now recomputes the preset time range every
10s when auto-refresh is on, so timeRange.end stays fresh instead of
being frozen at the moment the preset was clicked.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 09:57:43 +01:00
hsiegeln
1be303b801 feat: add application log panel to agent health page
All checks were successful
CI / build (push) Successful in 55s
CI / cleanup-branch (push) Has been skipped
CI / docker (push) Successful in 48s
CI / deploy (push) Successful in 37s
CI / deploy-feature (push) Has been skipped
Add the same log + timeline side-by-side layout from AgentInstance to
the AgentHealth page (/agents/{appId}). Includes search input, level
filter pills, sort toggle, and refresh button — matching the instance
page design exactly.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 22:54:07 +01:00
hsiegeln
d57249906a fix: refresh buttons use "now" as to-date for queries
All checks were successful
CI / build (push) Successful in 56s
CI / cleanup-branch (push) Has been skipped
CI / docker (push) Successful in 47s
CI / deploy (push) Successful in 41s
CI / deploy-feature (push) Has been skipped
Instead of calling refetch() with stale time params, the refresh
buttons now set a toOverride state to new Date().toISOString(). This
flows into the query key, triggering a fresh fetch with the current
time as the upper bound. Both useApplicationLogs and useAgentEvents
hooks accept an optional toOverride parameter.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 22:41:00 +01:00
hsiegeln
6a24dd01e9 fix: add exchange body fields to schema.d.ts for CI tsc check
All checks were successful
CI / cleanup-branch (push) Has been skipped
CI / build (push) Successful in 54s
CI / docker (push) Successful in 9s
CI / deploy (push) Successful in 19s
CI / deploy-feature (push) Has been skipped
The CI build runs tsc --noEmit which failed because the ExecutionDetail
type in schema.d.ts was missing the new inputBody/outputBody/inputHeaders/
outputHeaders fields added to the backend DTO.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 22:06:26 +01:00
hsiegeln
e10f021c54 use self hosted image for build
Some checks failed
CI / build (push) Failing after 26s
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
2026-03-25 22:03:19 +01:00
hsiegeln
b3c5e87230 fix: expose exchange body in API, fix RouteFlow index mapping
Some checks failed
CI / build (push) Failing after 25s
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
Add inputBody/outputBody/inputHeaders/outputHeaders to ExecutionDetail
DTO so exchange-level bodies are returned by the detail endpoint. Show
"Exchange Input" and "Exchange Output" panels on the detail page when
the data is available.

Fix RouteFlow node click selecting the wrong processor snapshot by
building a flowToTreeIndex mapping that correctly translates flow
display index → diagram node index → processorId → processor tree
index. Previously the diagram node index was used directly as the
processor tree index, which broke when the two orderings differed.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 22:02:26 +01:00
hsiegeln
9b63443842 feat: add sort toggle and refresh buttons to log/timeline panels
All checks were successful
CI / build (push) Successful in 55s
CI / cleanup-branch (push) Has been skipped
CI / docker (push) Successful in 50s
CI / deploy (push) Successful in 42s
CI / deploy-feature (push) Has been skipped
Remove auto-scroll override hack. Add sort order toggle (asc/desc
by time) and manual refresh button to both the application log and
agent events timeline panels on AgentInstance and AgentHealth pages.
Default is descending (newest first); toggling reverses the array.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 19:53:33 +01:00
hsiegeln
cd30c2d9b5 fix: match log/timeline height, DESC sort with scroll-to-top
All checks were successful
CI / build (push) Successful in 55s
CI / cleanup-branch (push) Has been skipped
CI / docker (push) Successful in 52s
CI / deploy (push) Successful in 39s
CI / deploy-feature (push) Has been skipped
Give logCard the same max-height and flex layout as timelineCard so
both columns are equal height. Revert .toReversed() so events stay
in DESC order (newest at top). Override EventFeed's auto-scroll-to-
bottom with a requestAnimationFrame that resets scrollTop to 0 after
mount, keeping newest entries visible at the top of both panels.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 19:12:08 +01:00
hsiegeln
b612941aae feat: wire up application logs from OpenSearch, fix event autoscroll
All checks were successful
CI / build (push) Successful in 55s
CI / cleanup-branch (push) Has been skipped
CI / docker (push) Successful in 51s
CI / deploy (push) Successful in 37s
CI / deploy-feature (push) Has been skipped
Add GET /api/v1/logs endpoint to query application logs stored in
OpenSearch with filters for application, agent, level, time range,
and text search. Wire up the AgentInstance LogViewer with real data
and an EventFeed-style toolbar (search input + level filter pills).

Fix agent events timeline autoscroll by reversing the DESC-ordered
events so newest entries appear at the bottom where EventFeed
autoscrolls to.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 18:56:13 +01:00
hsiegeln
20ee448f4e fix: OpenSearch status field mismatch, adopt RouteFlow flows prop
All checks were successful
CI / build (push) Successful in 56s
CI / cleanup-branch (push) Has been skipped
CI / docker (push) Successful in 1m43s
CI / deploy (push) Successful in 38s
CI / deploy-feature (push) Has been skipped
Fix admin OpenSearch page always showing "Disconnected" by aligning
frontend field names (reachable/nodeCount/host) with backend DTO.

Update design system to v0.1.10 and adopt the new multi-flow RouteFlow
API — error-handler nodes now render as labeled segments with error
variant instead of relying on legacy auto-separation.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 18:34:58 +01:00
hsiegeln
2bbca8ae38 fix: force SNAPSHOT update in Docker build (-U flag)
All checks were successful
CI / build (push) Successful in 55s
CI / cleanup-branch (push) Has been skipped
CI / docker (push) Successful in 40s
CI / deploy (push) Successful in 38s
CI / deploy-feature (push) Has been skipped
Same issue as the CI build — Docker layer cache can serve a stale
cameleer3-common SNAPSHOT.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 13:36:07 +01:00
hsiegeln
fea50b51ae fix: force SNAPSHOT update in CI build (-U flag)
Some checks failed
CI / build (push) Successful in 55s
CI / cleanup-branch (push) Has been skipped
CI / docker (push) Failing after 23s
CI / deploy (push) Has been skipped
CI / deploy-feature (push) Has been skipped
Maven cache can serve stale cameleer3-common SNAPSHOTs. The -U flag
forces Maven to check the remote registry for updated SNAPSHOTs on
every build.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 13:33:59 +01:00
79d37118e0 chore: use pre-baked build images from cameleer-build-images
Some checks failed
CI / build (push) Failing after 40s
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
Replace maven:3.9-eclipse-temurin-17 with cameleer-build:1 (includes
Node.js 22, curl, jq). Replace docker:27 with cameleer-docker-builder:1
(includes git, curl, jq). Removes per-build tool installation steps.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 13:26:11 +01:00
hsiegeln
7fd55ea8ba fix: remove core LogIndexService to fix CI snapshot resolution
Some checks failed
CI / build (push) Failing after 1m11s
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
LogIndexService in server-core imported LogEntry from cameleer3-common,
but the SNAPSHOT on the registry may not have it yet when the server CI
runs. Moved the dependency to server-app where both the controller and
OpenSearch implementation live.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 13:11:11 +01:00
hsiegeln
c96fbef5d5 ci: retry after cameleer3-common publish
Some checks failed
CI / build (push) Failing after 50s
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
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 13:05:23 +01:00
hsiegeln
7423e2ca14 feat: add application log ingestion with OpenSearch storage
Some checks failed
CI / cleanup-branch (push) Has been skipped
CI / build (push) Failing after 59s
CI / docker (push) Has been skipped
CI / deploy (push) Has been skipped
CI / deploy-feature (push) Has been skipped
Agents can now send application log entries in batches via POST /api/v1/data/logs.
Logs are indexed directly into OpenSearch daily indices (logs-{yyyy-MM-dd}) using
the bulk API. Index template defines explicit mappings for full-text search readiness.

New DTOs (LogEntry, LogBatch) added to cameleer3-common in the agent repo.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 11:53:27 +01:00
hsiegeln
bf600f8c5f fix: read version and updated_at from SQL columns in config repository
All checks were successful
CI / build (push) Successful in 12m13s
CI / cleanup-branch (push) Has been skipped
CI / docker (push) Successful in 44s
CI / deploy (push) Successful in 39s
CI / deploy-feature (push) Has been skipped
The findByApplication query only read config_val JSONB, ignoring the
version and updated_at SQL columns. The JSON blob contained version 0
from the original save, so agents saw no config and fell back to defaults.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 10:22:13 +01:00
hsiegeln
996ea65293 feat: LIVE/PAUSED toggle controls data fetching on sidebar navigation
All checks were successful
CI / build (push) Successful in 1m13s
CI / cleanup-branch (push) Has been skipped
CI / docker (push) Successful in 55s
CI / deploy (push) Successful in 39s
CI / deploy-feature (push) Has been skipped
LIVE: sidebar clicks trigger initial fetch + polling for the new route.
PAUSED: sidebar clicks navigate but queries are disabled — no fetches
until the user switches back to LIVE.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 10:01:14 +01:00
hsiegeln
9866dd5f23 fix: move design system dev install after COPY to bust Docker cache
All checks were successful
CI / build (push) Successful in 1m23s
CI / cleanup-branch (push) Has been skipped
CI / docker (push) Successful in 1m12s
CI / deploy (push) Successful in 38s
CI / deploy-feature (push) Has been skipped
The npm install @cameleer/design-system@dev was in the same cached layer
as npm ci, so Docker never re-ran it when the registry had a new version.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 09:37:51 +01:00
hsiegeln
d9c8816647 feat: add OpenSearch highlight snippets to search results
All checks were successful
CI / build (push) Successful in 1m23s
CI / cleanup-branch (push) Has been skipped
CI / docker (push) Successful in 54s
CI / deploy (push) Successful in 39s
CI / deploy-feature (push) Has been skipped
- Add highlight field to ExecutionSummary record
- Request highlight fragments from OpenSearch when full-text search is active
- Pass matchContext to command palette for display

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 09:29:07 +01:00
hsiegeln
b32c97c02b feat: fix Cmd-K shortcut and add exchange full-text search to command palette
All checks were successful
CI / build (push) Successful in 1m43s
CI / cleanup-branch (push) Has been skipped
CI / docker (push) Successful in 1m17s
CI / deploy (push) Successful in 40s
CI / deploy-feature (push) Has been skipped
- Add missing onOpen prop to CommandPalette (fixes Ctrl+K/Cmd+K)
- Wire server-side exchange search with debounced text query
- Use design system dev snapshot from Gitea registry in CI builds

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 08:57:24 +01:00
hsiegeln
552f02d25c fix: add JWT auth to application config API calls
All checks were successful
CI / build (push) Successful in 1m42s
CI / cleanup-branch (push) Has been skipped
CI / docker (push) Successful in 57s
CI / deploy (push) Successful in 39s
CI / deploy-feature (push) Has been skipped
Raw fetch() had no auth headers, causing 401s that silently broke tracing toggle.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 08:19:44 +01:00
hsiegeln
9f9968abab chore: upgrade cameleer3-common to 1.0-SNAPSHOT and enable snapshot resolution
All checks were successful
CI / build (push) Successful in 1m44s
CI / cleanup-branch (push) Has been skipped
CI / docker (push) Successful in 3m27s
CI / deploy (push) Successful in 39s
CI / deploy-feature (push) Has been skipped
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 08:04:29 +01:00
hsiegeln
69a3eb192f feat: persistent per-application config with GET/PUT endpoints
Some checks failed
CI / build (push) Failing after 1m10s
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
Add application_config table (V4 migration), repository, and REST
controller. GET /api/v1/config/{app} returns config, PUT saves and
pushes CONFIG_UPDATE to all LIVE agents via SSE. UI tracing toggle
now uses config API instead of direct SET_TRACED_PROCESSORS command.
Tracing store syncs with server config on load.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 07:42:55 +01:00
hsiegeln
488a32f319 feat: show tracing badges on processor nodes
All checks were successful
CI / build (push) Successful in 1m18s
CI / cleanup-branch (push) Has been skipped
CI / docker (push) Successful in 1m12s
CI / deploy (push) Successful in 40s
CI / deploy-feature (push) Has been skipped
Update design system to 0.1.8 and pass NodeBadge[] to both
ProcessorTimeline and RouteFlow. Traced processors display a
blue "TRACED" badge that updates reactively via Zustand store.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 23:10:37 +01:00
hsiegeln
bf57fd139b fix: show tracing action on all Flow view nodes
All checks were successful
CI / build (push) Successful in 1m26s
CI / cleanup-branch (push) Has been skipped
CI / docker (push) Successful in 53s
CI / deploy (push) Successful in 39s
CI / deploy-feature (push) Has been skipped
Use diagram node ID as fallback processorId when no processor
execution match exists (e.g. error handlers that didn't trigger).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 22:46:52 +01:00
hsiegeln
581d53a33e fix: match SET_TRACED_PROCESSORS payload to agent protocol
Some checks failed
CI / build (push) Successful in 1m28s
CI / cleanup-branch (push) Has been skipped
CI / docker (push) Successful in 55s
CI / deploy-feature (push) Has been cancelled
CI / deploy (push) Has been cancelled
Payload now sends {processors: {id: "BOTH"}} map instead of
{routeId, processorIds[]} array. Tracing state keyed by application
name (global, not per-route) matching agent behavior.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 22:43:55 +01:00
hsiegeln
f4dd2b3415 feat: add processor tracing toggle to exchange detail views
All checks were successful
CI / build (push) Successful in 1m22s
CI / cleanup-branch (push) Has been skipped
CI / docker (push) Successful in 52s
CI / deploy (push) Successful in 39s
CI / deploy-feature (push) Has been skipped
Wire getActions on ProcessorTimeline and RouteFlow to send
SET_TRACED_PROCESSORS commands to all agents of the same application.
Tracing state managed via Zustand store with optimistic UI and rollback.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 22:30:26 +01:00
hsiegeln
7532cc9d59 chore: update @cameleer/design-system to 0.1.7
All checks were successful
CI / build (push) Successful in 1m14s
CI / cleanup-branch (push) Has been skipped
CI / docker (push) Successful in 1m8s
CI / deploy (push) Successful in 39s
CI / deploy-feature (push) Has been skipped
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 21:59:40 +01:00
hsiegeln
e7590d72fd fix: restore Swagger UI on api-docs page
All checks were successful
CI / build (push) Successful in 1m23s
CI / cleanup-branch (push) Has been skipped
CI / docker (push) Successful in 50s
CI / deploy (push) Successful in 38s
CI / deploy-feature (push) Has been skipped
- Change Vite proxy pattern from /api to /api/ so /api-docs client
  route is not captured and proxied to the backend
- Fix SwaggerUIBundle init: remove empty presets/layout overrides that
  crashed the internal persistConfigs function
- Use correct CSS import (swagger-ui.css instead of index.css)
- Add requestInterceptor to auto-attach JWT token to Try-it-out calls
- Add swagger-ui-bundle to optimizeDeps.include for reliable loading
- Remove unused swagger-ui-dist.d.ts type declarations

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 20:53:48 +01:00
hsiegeln
57ce1db248 add metrics ingestion diagnostics and upgrade cameleer3-common to 0.0.3
All checks were successful
CI / build (push) Successful in 1m34s
CI / cleanup-branch (push) Has been skipped
CI / docker (push) Successful in 3m20s
CI / deploy (push) Successful in 39s
CI / deploy-feature (push) Has been skipped
- Add logging to MetricsController: warn on parse failures, debug on
  received metrics, buffer depth on 503
- Add GET /api/v1/admin/database/metrics-pipeline diagnostic endpoint
  (buffer depth, row count, distinct agents/metrics, latest timestamp)
- Fix BackpressureIT test JSON to match actual MetricsSnapshot schema
  (collectedAt/metricName/metricValue instead of timestamp/metrics)
- Upgrade cameleer3-common from 1.0-SNAPSHOT to 0.0.3 (adds engineLevel)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 20:23:26 +01:00
hsiegeln
c97d730a00 fix: show N/A for agent heap/CPU when no JVM metrics available
All checks were successful
CI / build (push) Successful in 1m22s
CI / cleanup-branch (push) Has been skipped
CI / docker (push) Successful in 55s
CI / deploy (push) Successful in 39s
CI / deploy-feature (push) Has been skipped
Indeterminate progress bars were misleading when agents don't report
JVM metrics — replaced with plain "N/A" text.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 19:46:58 +01:00
hsiegeln
581c4f9ad9 fix: restore registry URL in package-lock.json for CI
All checks were successful
CI / build (push) Successful in 1m16s
CI / cleanup-branch (push) Has been skipped
CI / docker (push) Successful in 1m12s
CI / deploy (push) Successful in 39s
CI / deploy-feature (push) Has been skipped
The lock file had "resolved": "../../design-system" from a local
install, causing npm ci in CI to silently skip the package.
Reinstalled from registry to fix the resolved URL.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 19:15:44 +01:00
hsiegeln
ef6bc4be21 fix: add npm registry auth token for UI build in CI
Some checks failed
CI / build (push) Failing after 39s
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
The Build UI step ran npm ci without authenticating to the Gitea npm
registry, causing @cameleer/design-system to fail to resolve. Add
REGISTRY_TOKEN to .npmrc before npm ci.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 19:12:35 +01:00
hsiegeln
8534bb8839 chore: upgrade @cameleer/design-system to v0.1.6
Some checks failed
CI / build (push) Failing after 39s
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
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 19:07:13 +01:00
hsiegeln
a5bc7cf6d1 fix: use self-portaling DetailPanel from design system v0.1.5
Some checks failed
CI / build (push) Failing after 57s
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
DetailPanel now portals itself to #cameleer-detail-panel-root (a div
AppShell places as a sibling of .main in the top-level flex row).
Pages just render <DetailPanel> inline — no manual createPortal,
no context, no prop drilling.

Remove the old #detail-panel-portal div from LayoutShell and the
createPortal wrappers from Dashboard and AgentHealth.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 19:00:02 +01:00
hsiegeln
5d2eff4f73 fix: normalize null fields from unconfigured OIDC response
All checks were successful
CI / build (push) Successful in 1m16s
CI / cleanup-branch (push) Has been skipped
CI / docker (push) Successful in 53s
CI / deploy (push) Successful in 40s
CI / deploy-feature (push) Has been skipped
When no OIDC config exists, the backend returns an object with all
null fields (via OidcAdminConfigResponse.unconfigured()). Normalize
all null values to sensible defaults when loading the form instead
of passing nulls through to Input components and .map() calls.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 18:44:02 +01:00
hsiegeln
9a4a4dc1af fix: handle null defaultRoles in OIDC config page
Some checks failed
CI / build (push) Has been cancelled
CI / docker (push) Has been cancelled
CI / deploy (push) Has been cancelled
CI / deploy-feature (push) Has been cancelled
CI / cleanup-branch (push) Has been cancelled
The API returns defaultRoles as null when no roles are configured.
Add null guards on all defaultRoles accesses to prevent .map() crash.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 18:41:59 +01:00
hsiegeln
f3241e904f fix: use createPortal for DetailPanel instead of context+useEffect
Some checks failed
CI / build (push) Successful in 1m21s
CI / cleanup-branch (push) Has been skipped
CI / docker (push) Successful in 53s
CI / deploy-feature (push) Has been cancelled
CI / deploy (push) Has been cancelled
The previous approach used useEffect+context to hoist DetailPanel
content to the AppShell level, but the dependency-free useEffect
caused a re-render loop that broke sidebar navigation.

Replace with createPortal: pages render DetailPanel inline in their
JSX but portal it to a target div (#detail-panel-portal) at the
AppShell level. No state lifting, no re-render loops.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 18:38:59 +01:00
hsiegeln
5de792744e fix: hoist DetailPanel into AppShell detail slot for proper slide-in
All checks were successful
CI / build (push) Successful in 1m22s
CI / cleanup-branch (push) Has been skipped
CI / docker (push) Successful in 51s
CI / deploy (push) Successful in 38s
CI / deploy-feature (push) Has been skipped
DetailPanel is a flex sibling that slides in from the right — it must
be rendered at the AppShell level via the detail prop, not inside the
page content. Add DetailPanelContext so pages can push their panel
content up to LayoutShell, which passes it to AppShell.detail.

Applied to Dashboard (exchange detail) and AgentHealth (instance detail).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 18:28:03 +01:00
hsiegeln
0a5f4a03b5 chore: upgrade @cameleer/design-system to v0.1.4
All checks were successful
CI / build (push) Successful in 1m13s
CI / cleanup-branch (push) Has been skipped
CI / docker (push) Successful in 1m11s
CI / deploy (push) Successful in 37s
CI / deploy-feature (push) Has been skipped
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 18:18:20 +01:00
hsiegeln
4ac11551c9 feat: add auto-refresh toggle wired to all polling queries
Some checks failed
CI / build (push) Failing after 51s
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
Upgrade @cameleer/design-system to ^0.1.3 which adds LIVE/PAUSED
toggle to TopBar backed by autoRefresh state in GlobalFilterProvider.

Add useRefreshInterval() hook that returns the polling interval when
auto-refresh is on, or false when paused. Wire it into all query
hooks that use refetchInterval (executions, catalog, agents, metrics,
admin database/opensearch).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 18:10:32 +01:00
hsiegeln
6fea5f2c5b fix: use .keyword suffix for text field sorting in OpenSearch
All checks were successful
CI / build (push) Successful in 1m22s
CI / cleanup-branch (push) Has been skipped
CI / docker (push) Successful in 44s
CI / deploy (push) Successful in 39s
CI / deploy-feature (push) Has been skipped
OpenSearch dynamically maps string fields as text with a .keyword
subfield. Sorting on text fields throws an error; only .keyword,
date, and numeric fields support sorting. Add .keyword suffix to
all string sort columns (status, routeId, agentId, executionId,
correlationId, applicationName) while keeping start_time and
duration_ms as-is.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 17:56:18 +01:00
hsiegeln
b7cac68ee1 fix: filter exchanges by application and restore snake_case sort columns
All checks were successful
CI / build (push) Successful in 1m23s
CI / cleanup-branch (push) Has been skipped
CI / docker (push) Successful in 41s
CI / deploy (push) Successful in 39s
CI / deploy-feature (push) Has been skipped
Add application_name filter to OpenSearch query builder — sidebar
app selection now correctly filters the exchange list. The
application field was being resolved to agentIds in the controller
but never applied as a query filter in OpenSearch.

Also restore snake_case sort column mapping since the OpenSearch
toMap() serializer uses snake_case field names (start_time, route_id,
etc.), not camelCase.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 17:41:07 +01:00
hsiegeln
cdbe330c47 fix: support all sortable columns and use camelCase for OpenSearch
All checks were successful
CI / build (push) Successful in 1m24s
CI / cleanup-branch (push) Has been skipped
CI / docker (push) Successful in 45s
CI / deploy (push) Successful in 37s
CI / deploy-feature (push) Has been skipped
Add executionId and applicationName to allowed sort fields. Fix sort
column mapping to use camelCase field names matching the OpenSearch
ExecutionDocument fields instead of snake_case DB column names. This
was causing sorts on most columns to either silently fall back to
startTime or return empty results from OpenSearch.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 17:37:01 +01:00
53e9073dca fix: update ExecutionRecord constructor in stats test for new fields
All checks were successful
CI / build (push) Successful in 1m13s
CI / cleanup-branch (push) Has been skipped
CI / docker (push) Successful in 1m9s
CI / deploy (push) Successful in 38s
CI / deploy-feature (push) Has been skipped
2026-03-24 17:26:07 +01:00
b8c316727e fix: update ExecutionRecord constructor calls in tests for new fields
Some checks failed
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
CI / cleanup-branch (push) Has been cancelled
2026-03-24 17:25:48 +01:00
hsiegeln
48455cd559 fix: use server-side sorting for paginated tables
Some checks failed
CI / cleanup-branch (push) Has been skipped
CI / build (push) Failing after 1m10s
CI / docker (push) Has been skipped
CI / deploy (push) Has been skipped
CI / deploy-feature (push) Has been skipped
Upgrade @cameleer/design-system to v0.1.1 which adds onSortChange
callback to DataTable. Wire it up in Dashboard (exchanges), AuditLog,
and RouteDetail (recent executions) so sorting triggers a new API
request with sortField/sortDir instead of only sorting the current page.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 17:05:17 +01:00
aa3d9f375b Merge pull request 'feat: agent protocol v2 — engine levels, enriched acks, route snapshots' (#91) from fix/agent-protocol-v2 into main
Some checks failed
CI / build (push) Failing after 1m0s
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
Reviewed-on: cameleer/cameleer3-server#91
2026-03-24 16:50:09 +01:00
hsiegeln
e54d20bcb7 feat: migrate login page to design system styling
All checks were successful
CI / build (push) Successful in 1m26s
CI / cleanup-branch (push) Has been skipped
CI / docker (push) Successful in 57s
CI / deploy (push) Successful in 38s
CI / deploy-feature (push) Has been skipped
Replace inline styles with CSS module matching the design system's
LoginForm visual patterns. Uses proper DS class structure (divider,
social section, form fields) while keeping username-based auth
instead of the DS component's email validation.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 16:44:52 +01:00
hsiegeln
81f85aa82d feat: replace UI with design system example pages wired to real API
Some checks failed
CI / build (push) Successful in 1m18s
CI / cleanup-branch (push) Has been skipped
CI / docker (push) Successful in 55s
CI / deploy-feature (push) Has been cancelled
CI / deploy (push) Has been cancelled
Migrate all page components from the @cameleer/design-system v0.0.3
example UI, replacing mock data with real backend API hooks. This brings
richer visuals (KpiStrip, GroupCard, RouteFlow, ProcessorTimeline,
DateRangePicker, expandable rows) while preserving all existing API
integration, auth, and routing infrastructure.

Pages migrated: Dashboard, RoutesMetrics, RouteDetail, ExchangeDetail,
AgentHealth, AgentInstance, OidcConfig, AuditLog, RBAC (Users/Groups/Roles).
Also enhanced LayoutShell CommandPalette with real search data from catalog.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 16:42:16 +01:00
2887fe9599 feat: add V3 migration for engine_level and route-level snapshot columns
Some checks failed
CI / build (push) Failing after 51s
CI / cleanup-branch (push) Has been skipped
CI / build (pull_request) Failing after 52s
CI / cleanup-branch (pull_request) Has been skipped
CI / docker (push) Has been skipped
CI / docker (pull_request) Has been skipped
CI / deploy (push) Has been skipped
CI / deploy-feature (push) Has been skipped
CI / deploy (pull_request) Has been skipped
CI / deploy-feature (pull_request) Has been skipped
2026-03-24 16:13:11 +01:00
b1679b110c feat: add engine_level and route-level snapshot columns to PostgresExecutionStore
Some checks failed
CI / docker (push) Has been cancelled
CI / build (push) Has been cancelled
CI / deploy (push) Has been cancelled
CI / deploy-feature (push) Has been cancelled
CI / cleanup-branch (push) Has been cancelled
Add engine_level, input_body, output_body, input_headers, output_headers
to the executions INSERT/SELECT/UPSERT and row mapper. Required for
REGULAR mode where route-level payloads exist but no processor records.

Note: requires ALTER TABLE migration to add the new columns.
2026-03-24 16:12:46 +01:00
e7835e1100 feat: map engineLevel and route-level snapshots in IngestionService
Some checks failed
CI / docker (push) Has been cancelled
CI / deploy (push) Has been cancelled
CI / deploy-feature (push) Has been cancelled
CI / cleanup-branch (push) Has been cancelled
CI / build (push) Has been cancelled
Extract inputBody/outputBody/inputHeaders/outputHeaders from RouteExecution
snapshots and pass to ExecutionRecord. Maps engineLevel field. Critical for
REGULAR mode where no processor records exist but route-level payloads do.
2026-03-24 16:11:55 +01:00
ed65b87af2 feat: add engineLevel and route-level snapshot fields to ExecutionRecord
Some checks failed
CI / docker (push) Has been cancelled
CI / deploy (push) Has been cancelled
CI / deploy-feature (push) Has been cancelled
CI / cleanup-branch (push) Has been cancelled
CI / build (push) Has been cancelled
Adds engineLevel (NONE/MINIMAL/REGULAR/COMPLETE) and inputBody/outputBody/
inputHeaders/outputHeaders to ExecutionRecord so REGULAR mode route-level
payloads are persisted (previously only processor-level records had payloads).
2026-03-24 16:11:26 +01:00
4a99e6cf6b feat: support enriched command ack with status/message + set-traced-processors command type
Some checks failed
CI / docker (push) Has been cancelled
CI / deploy (push) Has been cancelled
CI / deploy-feature (push) Has been cancelled
CI / cleanup-branch (push) Has been cancelled
CI / build (push) Has been cancelled
- Add @RequestBody(required=false) CommandAckRequest to ack endpoint for
  receiving agent command results (backward compat with old agents)
- Record command results in agent event log via AgentEventService
- Add set-traced-processors to mapCommandType switch
- Inject AgentEventService dependency
2026-03-24 16:11:04 +01:00
4d9a9ff851 feat: add CommandAckRequest DTO for enriched command acknowledgments
Some checks failed
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
CI / cleanup-branch (push) Has been cancelled
2026-03-24 16:10:27 +01:00
292a38fe30 feat: add SET_TRACED_PROCESSORS command type for per-processor overrides
Some checks failed
CI / docker (push) Has been cancelled
CI / deploy (push) Has been cancelled
CI / deploy-feature (push) Has been cancelled
CI / cleanup-branch (push) Has been cancelled
CI / build (push) Has been cancelled
2026-03-24 16:10:21 +01:00
279 changed files with 31678 additions and 3905 deletions

View File

@@ -14,16 +14,11 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: github.event_name != 'delete' if: github.event_name != 'delete'
container: container:
image: maven:3.9-eclipse-temurin-17 image: gitea.siegeln.net/cameleer/cameleer-build:1
credentials:
username: cameleer
password: ${{ secrets.REGISTRY_TOKEN }}
steps: steps:
- name: Install Node.js 22
run: |
apt-get update && apt-get install -y ca-certificates curl gnupg
mkdir -p /etc/apt/keyrings
curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg
echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_22.x nodistro main" > /etc/apt/sources.list.d/nodesource.list
apt-get update && apt-get install -y nodejs
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Configure Gitea Maven Registry - name: Configure Gitea Maven Registry
@@ -53,22 +48,27 @@ jobs:
- name: Build UI - name: Build UI
working-directory: ui working-directory: ui
run: | run: |
echo '//gitea.siegeln.net/api/packages/cameleer/npm/:_authToken=${REGISTRY_TOKEN}' >> .npmrc
npm ci npm ci
npm run build npm run build
env:
REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }}
- name: Build and Test - name: Build and Test
run: mvn clean verify -DskipITs --batch-mode run: mvn clean verify -DskipITs -U --batch-mode
docker: docker:
needs: build needs: build
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: github.event_name == 'push' if: github.event_name == 'push'
container: container:
image: docker:27 image: gitea.siegeln.net/cameleer/cameleer-docker-builder:1
credentials:
username: cameleer
password: ${{ secrets.REGISTRY_TOKEN }}
steps: steps:
- name: Checkout - name: Checkout
run: | run: |
apk add --no-cache git
git clone --depth=1 --branch=${GITHUB_REF_NAME} https://cameleer:${REGISTRY_TOKEN}@gitea.siegeln.net/${GITHUB_REPOSITORY}.git . git clone --depth=1 --branch=${GITHUB_REF_NAME} https://cameleer:${REGISTRY_TOKEN}@gitea.siegeln.net/${GITHUB_REPOSITORY}.git .
env: env:
REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }} REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }}
@@ -95,7 +95,7 @@ jobs:
echo "IMAGE_TAGS=branch-$SLUG" >> "$GITHUB_ENV" echo "IMAGE_TAGS=branch-$SLUG" >> "$GITHUB_ENV"
fi fi
- name: Set up QEMU for cross-platform builds - name: Set up QEMU for cross-platform builds
run: docker run --rm --privileged tonistiigi/binfmt --install all run: docker run --rm --privileged gitea.siegeln.net/cameleer/binfmt:1 --install all
- name: Build and push server - name: Build and push server
run: | run: |
docker buildx create --use --name cibuilder docker buildx create --use --name cibuilder
@@ -133,7 +133,6 @@ jobs:
if: always() if: always()
- name: Cleanup old container images - name: Cleanup old container images
run: | run: |
apk add --no-cache curl jq
API="https://gitea.siegeln.net/api/v1" API="https://gitea.siegeln.net/api/v1"
AUTH="Authorization: token ${REGISTRY_TOKEN}" AUTH="Authorization: token ${REGISTRY_TOKEN}"
CURRENT_SHA="${{ github.sha }}" CURRENT_SHA="${{ github.sha }}"

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
.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,9 +36,9 @@ java -jar cameleer3-server-app/target/cameleer3-server-app-1.0-SNAPSHOT.jar
- Spring Boot 3.4.3 parent POM - Spring Boot 3.4.3 parent POM
- Depends on `com.cameleer3:cameleer3-common` from Gitea Maven registry - Depends on `com.cameleer3:cameleer3-common` from Gitea Maven registry
- Jackson `JavaTimeModule` for `Instant` deserialization - Jackson `JavaTimeModule` for `Instant` deserialization
- Communication: receives HTTP POST data from agents, 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 - Maintains agent instance registry with states: LIVE → STALE → DEAD
- Storage: PostgreSQL (TimescaleDB) for structured data, OpenSearch for full-text search - 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 - Security: JWT auth with RBAC (AGENT/VIEWER/OPERATOR/ADMIN roles), Ed25519 config signing, bootstrap token for registration
- OIDC: Optional external identity provider support (token exchange pattern). Configured via admin API, stored in database (`server_config` table) - OIDC: Optional external identity provider support (token exchange pattern). Configured via admin API, stored in database (`server_config` table)
- User persistence: PostgreSQL `users` table, admin CRUD at `/api/v1/admin/users` - User persistence: PostgreSQL `users` table, admin CRUD at `/api/v1/admin/users`
@@ -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` - 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 - 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 ## Disabled Skills
- Do NOT use any `gsd:*` skills in this project. This includes all `/gsd:` prefixed commands. - Do NOT use any `gsd:*` skills in this project. This includes all `/gsd:` prefixed commands.

View File

@@ -12,7 +12,7 @@ COPY cameleer3-server-app/pom.xml cameleer3-server-app/
# Cache deps — only re-downloaded when POMs change # Cache deps — only re-downloaded when POMs change
RUN mvn dependency:go-offline -B || true RUN mvn dependency:go-offline -B || true
COPY . . COPY . .
RUN mvn clean package -DskipTests -B RUN mvn clean package -DskipTests -U -B
FROM eclipse-temurin:17-jre FROM eclipse-temurin:17-jre
WORKDIR /app WORKDIR /app

View File

@@ -100,7 +100,7 @@ JWTs carry a `roles` claim. Endpoints are restricted by role:
| Role | Access | | Role | Access |
|------|--------| |------|--------|
| `AGENT` | Data ingestion (`/data/**`), heartbeat, SSE events, command ack | | `AGENT` | Data ingestion (`/data/**` — executions, diagrams, metrics, logs), heartbeat, SSE events, command ack |
| `VIEWER` | Search, execution detail, diagrams, agent list | | `VIEWER` | Search, execution detail, diagrams, agent list |
| `OPERATOR` | VIEWER + send commands to agents | | `OPERATOR` | VIEWER + send commands to agents |
| `ADMIN` | OPERATOR + user management (`/admin/**`) | | `ADMIN` | OPERATOR + user management (`/admin/**`) |
@@ -220,6 +220,20 @@ curl -s -X POST http://localhost:8081/api/v1/data/metrics \
-H "X-Protocol-Version: 1" \ -H "X-Protocol-Version: 1" \
-H "Authorization: Bearer $TOKEN" \ -H "Authorization: Bearer $TOKEN" \
-d '[{"agentId":"agent-1","metricName":"cpu","value":42.0,"timestamp":"2026-03-11T00:00:00Z","tags":{}}]' -d '[{"agentId":"agent-1","metricName":"cpu","value":42.0,"timestamp":"2026-03-11T00:00:00Z","tags":{}}]'
# Post application log entries (batch)
curl -s -X POST http://localhost:8081/api/v1/data/logs \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $TOKEN" \
-d '{
"entries": [{
"timestamp": "2026-03-25T10:00:00Z",
"level": "INFO",
"loggerName": "com.acme.MyService",
"message": "Processing order #12345",
"threadName": "main"
}]
}'
``` ```
**Note:** The `X-Protocol-Version: 1` header is required on all `/api/v1/data/**` endpoints. Missing or wrong version returns 400. **Note:** The `X-Protocol-Version: 1` header is required on all `/api/v1/data/**` endpoints. Missing or wrong version returns 400.
@@ -311,6 +325,12 @@ curl -s -X POST http://localhost:8081/api/v1/agents/groups/order-service-prod/co
-H "Authorization: Bearer $TOKEN" \ -H "Authorization: Bearer $TOKEN" \
-d '{"type":"deep-trace","payload":{"routeId":"route-1","durationSeconds":60}}' -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 # Broadcast command to all live agents
curl -s -X POST http://localhost:8081/api/v1/agents/commands \ curl -s -X POST http://localhost:8081/api/v1/agents/commands \
-H "Content-Type: application/json" \ -H "Content-Type: application/json" \
@@ -324,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. **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. **Command expiry:** Unacknowledged commands expire after 60 seconds.
@@ -361,6 +381,8 @@ Key settings in `cameleer3-server-app/src/main/resources/application.yml`:
| `security.oidc.client-secret` | | OAuth2 client secret (`CAMELEER_OIDC_CLIENT_SECRET`) | | `security.oidc.client-secret` | | OAuth2 client secret (`CAMELEER_OIDC_CLIENT_SECRET`) |
| `security.oidc.roles-claim` | `realm_access.roles` | JSONPath to roles in OIDC id_token (`CAMELEER_OIDC_ROLES_CLAIM`) | | `security.oidc.roles-claim` | `realm_access.roles` | JSONPath to roles in OIDC id_token (`CAMELEER_OIDC_ROLES_CLAIM`) |
| `security.oidc.default-roles` | `VIEWER` | Default roles for new OIDC users (`CAMELEER_OIDC_DEFAULT_ROLES`) | | `security.oidc.default-roles` | `VIEWER` | Default roles for new OIDC users (`CAMELEER_OIDC_DEFAULT_ROLES`) |
| `opensearch.log-index-prefix` | `logs-` | OpenSearch index prefix for application logs (`CAMELEER_LOG_INDEX_PREFIX`) |
| `opensearch.log-retention-days` | `7` | Days before log indices are deleted (`CAMELEER_LOG_RETENTION_DAYS`) |
## Web UI Development ## Web UI Development

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

@@ -1,5 +1,6 @@
package com.cameleer3.server.app.config; package com.cameleer3.server.app.config;
import com.cameleer3.server.app.interceptor.AuditInterceptor;
import com.cameleer3.server.app.interceptor.ProtocolVersionInterceptor; import com.cameleer3.server.app.interceptor.ProtocolVersionInterceptor;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
@@ -7,17 +8,17 @@ import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/** /**
* Web MVC configuration. * Web MVC configuration.
* <p>
* Registers the {@link ProtocolVersionInterceptor} on data and agent endpoint paths,
* excluding health, API docs, and Swagger UI paths that do not require protocol versioning.
*/ */
@Configuration @Configuration
public class WebConfig implements WebMvcConfigurer { public class WebConfig implements WebMvcConfigurer {
private final ProtocolVersionInterceptor protocolVersionInterceptor; private final ProtocolVersionInterceptor protocolVersionInterceptor;
private final AuditInterceptor auditInterceptor;
public WebConfig(ProtocolVersionInterceptor protocolVersionInterceptor) { public WebConfig(ProtocolVersionInterceptor protocolVersionInterceptor,
AuditInterceptor auditInterceptor) {
this.protocolVersionInterceptor = protocolVersionInterceptor; this.protocolVersionInterceptor = protocolVersionInterceptor;
this.auditInterceptor = auditInterceptor;
} }
@Override @Override
@@ -33,5 +34,14 @@ public class WebConfig implements WebMvcConfigurer {
"/api/v1/agents/register", "/api/v1/agents/register",
"/api/v1/agents/*/refresh" "/api/v1/agents/*/refresh"
); );
// Safety-net audit: catches any unaudited POST/PUT/DELETE
registry.addInterceptor(auditInterceptor)
.addPathPatterns("/api/v1/**")
.excludePathPatterns(
"/api/v1/data/**",
"/api/v1/agents/*/heartbeat",
"/api/v1/health"
);
} }
} }

View File

@@ -1,16 +1,25 @@
package com.cameleer3.server.app.controller; package com.cameleer3.server.app.controller;
import com.cameleer3.server.app.agent.SseConnectionManager; import com.cameleer3.server.app.agent.SseConnectionManager;
import com.cameleer3.server.app.dto.CommandAckRequest;
import com.cameleer3.server.app.dto.CommandBroadcastResponse; import com.cameleer3.server.app.dto.CommandBroadcastResponse;
import com.cameleer3.server.app.dto.CommandRequest; import com.cameleer3.server.app.dto.CommandRequest;
import com.cameleer3.server.app.dto.CommandSingleResponse; 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;
import com.cameleer3.server.core.agent.AgentCommand; import com.cameleer3.server.core.agent.AgentCommand;
import com.cameleer3.server.core.agent.AgentEventService;
import com.cameleer3.server.core.agent.AgentInfo; import com.cameleer3.server.core.agent.AgentInfo;
import com.cameleer3.server.core.agent.AgentRegistryService; import com.cameleer3.server.core.agent.AgentRegistryService;
import com.cameleer3.server.core.agent.AgentState; import com.cameleer3.server.core.agent.AgentState;
import com.cameleer3.server.core.agent.CommandReply;
import com.cameleer3.server.core.agent.CommandType; import com.cameleer3.server.core.agent.CommandType;
import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.http.HttpServletRequest;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
@@ -26,7 +35,14 @@ import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.server.ResponseStatusException; import org.springframework.web.server.ResponseStatusException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List; 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. * Command push endpoints for sending commands to agents via SSE.
@@ -48,23 +64,30 @@ public class AgentCommandController {
private final AgentRegistryService registryService; private final AgentRegistryService registryService;
private final SseConnectionManager connectionManager; private final SseConnectionManager connectionManager;
private final ObjectMapper objectMapper; private final ObjectMapper objectMapper;
private final AgentEventService agentEventService;
private final AuditService auditService;
public AgentCommandController(AgentRegistryService registryService, public AgentCommandController(AgentRegistryService registryService,
SseConnectionManager connectionManager, SseConnectionManager connectionManager,
ObjectMapper objectMapper) { ObjectMapper objectMapper,
AgentEventService agentEventService,
AuditService auditService) {
this.registryService = registryService; this.registryService = registryService;
this.connectionManager = connectionManager; this.connectionManager = connectionManager;
this.objectMapper = objectMapper; this.objectMapper = objectMapper;
this.agentEventService = agentEventService;
this.auditService = auditService;
} }
@PostMapping("/{id}/commands") @PostMapping("/{id}/commands")
@Operation(summary = "Send command to a specific agent", @Operation(summary = "Send command to a specific agent",
description = "Sends a config-update, deep-trace, or replay command to the specified agent") description = "Sends a command to the specified agent via SSE")
@ApiResponse(responseCode = "202", description = "Command accepted") @ApiResponse(responseCode = "202", description = "Command accepted")
@ApiResponse(responseCode = "400", description = "Invalid command payload") @ApiResponse(responseCode = "400", description = "Invalid command payload")
@ApiResponse(responseCode = "404", description = "Agent not registered") @ApiResponse(responseCode = "404", description = "Agent not registered")
public ResponseEntity<CommandSingleResponse> sendCommand(@PathVariable String id, public ResponseEntity<CommandSingleResponse> sendCommand(@PathVariable String id,
@RequestBody CommandRequest request) throws JsonProcessingException { @RequestBody CommandRequest request,
HttpServletRequest httpRequest) throws JsonProcessingException {
AgentInfo agent = registryService.findById(id); AgentInfo agent = registryService.findById(id);
if (agent == null) { if (agent == null) {
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Agent not found: " + id); throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Agent not found: " + id);
@@ -76,6 +99,10 @@ public class AgentCommandController {
String status = connectionManager.isConnected(id) ? "DELIVERED" : "PENDING"; String status = connectionManager.isConnected(id) ? "DELIVERED" : "PENDING";
auditService.log("send_agent_command", AuditCategory.AGENT, id,
java.util.Map.of("type", request.type(), "status", status),
AuditResult.SUCCESS, httpRequest);
return ResponseEntity.status(HttpStatus.ACCEPTED) return ResponseEntity.status(HttpStatus.ACCEPTED)
.body(new CommandSingleResponse(command.id(), status)); .body(new CommandSingleResponse(command.id(), status));
} }
@@ -86,7 +113,8 @@ public class AgentCommandController {
@ApiResponse(responseCode = "202", description = "Commands accepted") @ApiResponse(responseCode = "202", description = "Commands accepted")
@ApiResponse(responseCode = "400", description = "Invalid command payload") @ApiResponse(responseCode = "400", description = "Invalid command payload")
public ResponseEntity<CommandBroadcastResponse> sendGroupCommand(@PathVariable String group, public ResponseEntity<CommandBroadcastResponse> sendGroupCommand(@PathVariable String group,
@RequestBody CommandRequest request) throws JsonProcessingException { @RequestBody CommandRequest request,
HttpServletRequest httpRequest) throws JsonProcessingException {
CommandType type = mapCommandType(request.type()); CommandType type = mapCommandType(request.type());
String payloadJson = request.payload() != null ? objectMapper.writeValueAsString(request.payload()) : "{}"; String payloadJson = request.payload() != null ? objectMapper.writeValueAsString(request.payload()) : "{}";
@@ -101,6 +129,10 @@ public class AgentCommandController {
commandIds.add(command.id()); commandIds.add(command.id());
} }
auditService.log("broadcast_group_command", AuditCategory.AGENT, group,
java.util.Map.of("type", request.type(), "agentCount", agents.size()),
AuditResult.SUCCESS, httpRequest);
return ResponseEntity.status(HttpStatus.ACCEPTED) return ResponseEntity.status(HttpStatus.ACCEPTED)
.body(new CommandBroadcastResponse(commandIds, agents.size())); .body(new CommandBroadcastResponse(commandIds, agents.size()));
} }
@@ -110,7 +142,8 @@ public class AgentCommandController {
description = "Sends a command to all agents currently in LIVE state") description = "Sends a command to all agents currently in LIVE state")
@ApiResponse(responseCode = "202", description = "Commands accepted") @ApiResponse(responseCode = "202", description = "Commands accepted")
@ApiResponse(responseCode = "400", description = "Invalid command payload") @ApiResponse(responseCode = "400", description = "Invalid command payload")
public ResponseEntity<CommandBroadcastResponse> broadcastCommand(@RequestBody CommandRequest request) throws JsonProcessingException { public ResponseEntity<CommandBroadcastResponse> broadcastCommand(@RequestBody CommandRequest request,
HttpServletRequest httpRequest) throws JsonProcessingException {
CommandType type = mapCommandType(request.type()); CommandType type = mapCommandType(request.type());
String payloadJson = request.payload() != null ? objectMapper.writeValueAsString(request.payload()) : "{}"; String payloadJson = request.payload() != null ? objectMapper.writeValueAsString(request.payload()) : "{}";
@@ -122,31 +155,124 @@ public class AgentCommandController {
commandIds.add(command.id()); commandIds.add(command.id());
} }
auditService.log("broadcast_all_command", AuditCategory.AGENT, null,
java.util.Map.of("type", request.type(), "agentCount", liveAgents.size()),
AuditResult.SUCCESS, httpRequest);
return ResponseEntity.status(HttpStatus.ACCEPTED) return ResponseEntity.status(HttpStatus.ACCEPTED)
.body(new CommandBroadcastResponse(commandIds, liveAgents.size())); .body(new CommandBroadcastResponse(commandIds, liveAgents.size()));
} }
@PostMapping("/{id}/commands/{commandId}/ack") @PostMapping("/{id}/commands/{commandId}/ack")
@Operation(summary = "Acknowledge command receipt", @Operation(summary = "Acknowledge command receipt",
description = "Agent acknowledges that it has received and processed a command") description = "Agent acknowledges that it has received and processed a command, with result status and message")
@ApiResponse(responseCode = "200", description = "Command acknowledged") @ApiResponse(responseCode = "200", description = "Command acknowledged")
@ApiResponse(responseCode = "404", description = "Command not found") @ApiResponse(responseCode = "404", description = "Command not found")
public ResponseEntity<Void> acknowledgeCommand(@PathVariable String id, public ResponseEntity<Void> acknowledgeCommand(@PathVariable String id,
@PathVariable String commandId) { @PathVariable String commandId,
@RequestBody(required = false) CommandAckRequest body) {
boolean acknowledged = registryService.acknowledgeCommand(id, commandId); boolean acknowledged = registryService.acknowledgeCommand(id, commandId);
if (!acknowledged) { if (!acknowledged) {
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Command not found: " + commandId); throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Command not found: " + commandId);
} }
// Complete any pending reply future (for synchronous request-reply commands like TEST_EXPRESSION)
registryService.completeReply(commandId,
body != null ? body.status() : "SUCCESS",
body != null ? body.message() : null,
body != null ? body.data() : null);
// Record command result in agent event log
if (body != null && body.status() != null) {
AgentInfo agent = registryService.findById(id);
String application = agent != null ? agent.application() : "unknown";
agentEventService.recordEvent(id, application, "COMMAND_" + body.status(),
"Command " + commandId + ": " + body.message());
log.debug("Command {} ack from agent {}: {} - {}", commandId, id, body.status(), body.message());
}
return ResponseEntity.ok().build(); 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) { private CommandType mapCommandType(String typeStr) {
return switch (typeStr) { return switch (typeStr) {
case "config-update" -> CommandType.CONFIG_UPDATE; case "config-update" -> CommandType.CONFIG_UPDATE;
case "deep-trace" -> CommandType.DEEP_TRACE; case "deep-trace" -> CommandType.DEEP_TRACE;
case "replay" -> CommandType.REPLAY; 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, default -> throw new ResponseStatusException(HttpStatus.BAD_REQUEST,
"Invalid command type: " + typeStr + ". Valid: config-update, deep-trace, replay"); "Invalid command type: " + typeStr + ". Valid: config-update, deep-trace, replay, set-traced-processors, test-expression, route-control");
}; };
} }
} }

View File

@@ -8,6 +8,9 @@ import com.cameleer3.server.app.dto.AgentRegistrationRequest;
import com.cameleer3.server.app.dto.AgentRegistrationResponse; import com.cameleer3.server.app.dto.AgentRegistrationResponse;
import com.cameleer3.server.app.dto.ErrorResponse; import com.cameleer3.server.app.dto.ErrorResponse;
import com.cameleer3.server.app.security.BootstrapTokenValidator; import com.cameleer3.server.app.security.BootstrapTokenValidator;
import com.cameleer3.server.core.admin.AuditCategory;
import com.cameleer3.server.core.admin.AuditResult;
import com.cameleer3.server.core.admin.AuditService;
import com.cameleer3.server.core.agent.AgentEventService; import com.cameleer3.server.core.agent.AgentEventService;
import com.cameleer3.server.core.agent.AgentInfo; import com.cameleer3.server.core.agent.AgentInfo;
import com.cameleer3.server.core.agent.AgentRegistryService; import com.cameleer3.server.core.agent.AgentRegistryService;
@@ -58,6 +61,7 @@ public class AgentRegistrationController {
private final JwtService jwtService; private final JwtService jwtService;
private final Ed25519SigningService ed25519SigningService; private final Ed25519SigningService ed25519SigningService;
private final AgentEventService agentEventService; private final AgentEventService agentEventService;
private final AuditService auditService;
private final JdbcTemplate jdbc; private final JdbcTemplate jdbc;
public AgentRegistrationController(AgentRegistryService registryService, public AgentRegistrationController(AgentRegistryService registryService,
@@ -66,6 +70,7 @@ public class AgentRegistrationController {
JwtService jwtService, JwtService jwtService,
Ed25519SigningService ed25519SigningService, Ed25519SigningService ed25519SigningService,
AgentEventService agentEventService, AgentEventService agentEventService,
AuditService auditService,
JdbcTemplate jdbc) { JdbcTemplate jdbc) {
this.registryService = registryService; this.registryService = registryService;
this.config = config; this.config = config;
@@ -73,6 +78,7 @@ public class AgentRegistrationController {
this.jwtService = jwtService; this.jwtService = jwtService;
this.ed25519SigningService = ed25519SigningService; this.ed25519SigningService = ed25519SigningService;
this.agentEventService = agentEventService; this.agentEventService = agentEventService;
this.auditService = auditService;
this.jdbc = jdbc; this.jdbc = jdbc;
} }
@@ -113,6 +119,10 @@ public class AgentRegistrationController {
agentEventService.recordEvent(request.agentId(), application, "REGISTERED", agentEventService.recordEvent(request.agentId(), application, "REGISTERED",
"Agent registered: " + request.name()); "Agent registered: " + request.name());
auditService.log(request.agentId(), "agent_register", AuditCategory.AGENT, request.agentId(),
Map.of("application", application, "name", request.name()),
AuditResult.SUCCESS, httpRequest);
// Issue JWT tokens with AGENT role // Issue JWT tokens with AGENT role
List<String> roles = List.of("AGENT"); List<String> roles = List.of("AGENT");
String accessToken = jwtService.createAccessToken(request.agentId(), application, roles); String accessToken = jwtService.createAccessToken(request.agentId(), application, roles);
@@ -135,7 +145,8 @@ public class AgentRegistrationController {
@ApiResponse(responseCode = "401", description = "Invalid or expired refresh token") @ApiResponse(responseCode = "401", description = "Invalid or expired refresh token")
@ApiResponse(responseCode = "404", description = "Agent not found") @ApiResponse(responseCode = "404", description = "Agent not found")
public ResponseEntity<AgentRefreshResponse> refresh(@PathVariable String id, public ResponseEntity<AgentRefreshResponse> refresh(@PathVariable String id,
@RequestBody AgentRefreshRequest request) { @RequestBody AgentRefreshRequest request,
HttpServletRequest httpRequest) {
if (request.refreshToken() == null || request.refreshToken().isBlank()) { if (request.refreshToken() == null || request.refreshToken().isBlank()) {
return ResponseEntity.status(401).build(); return ResponseEntity.status(401).build();
} }
@@ -169,6 +180,9 @@ public class AgentRegistrationController {
String newAccessToken = jwtService.createAccessToken(agentId, agent.application(), roles); String newAccessToken = jwtService.createAccessToken(agentId, agent.application(), roles);
String newRefreshToken = jwtService.createRefreshToken(agentId, agent.application(), roles); String newRefreshToken = jwtService.createRefreshToken(agentId, agent.application(), roles);
auditService.log(agentId, "agent_token_refresh", AuditCategory.AUTH, agentId,
null, AuditResult.SUCCESS, httpRequest);
return ResponseEntity.ok(new AgentRefreshResponse(newAccessToken, newRefreshToken)); return ResponseEntity.ok(new AgentRefreshResponse(newAccessToken, newRefreshToken));
} }

View File

@@ -14,7 +14,8 @@ public class ApiExceptionHandler {
@ExceptionHandler(ResponseStatusException.class) @ExceptionHandler(ResponseStatusException.class)
public ResponseEntity<ErrorResponse> handleResponseStatus(ResponseStatusException ex) { public ResponseEntity<ErrorResponse> handleResponseStatus(ResponseStatusException ex) {
String reason = ex.getReason();
return ResponseEntity.status(ex.getStatusCode()) 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

@@ -0,0 +1,208 @@
package com.cameleer3.server.app.controller;
import com.cameleer3.common.model.ApplicationConfig;
import com.cameleer3.server.app.dto.TestExpressionRequest;
import com.cameleer3.server.app.dto.TestExpressionResponse;
import com.cameleer3.server.app.storage.PostgresApplicationConfigRepository;
import com.cameleer3.server.core.admin.AuditCategory;
import com.cameleer3.server.core.admin.AuditResult;
import com.cameleer3.server.core.admin.AuditService;
import com.cameleer3.server.core.agent.AgentCommand;
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.cameleer3.server.core.storage.DiagramStore;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletRequest;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
/**
* Per-application configuration management.
* Agents fetch config at startup; the UI modifies config which is persisted and pushed to agents via SSE.
*/
@RestController
@RequestMapping("/api/v1/config")
@Tag(name = "Application Config", description = "Per-application observability configuration")
public class ApplicationConfigController {
private static final Logger log = LoggerFactory.getLogger(ApplicationConfigController.class);
private final PostgresApplicationConfigRepository configRepository;
private final AgentRegistryService registryService;
private final ObjectMapper objectMapper;
private final AuditService auditService;
private final DiagramStore diagramStore;
public ApplicationConfigController(PostgresApplicationConfigRepository configRepository,
AgentRegistryService registryService,
ObjectMapper objectMapper,
AuditService auditService,
DiagramStore diagramStore) {
this.configRepository = configRepository;
this.registryService = registryService;
this.objectMapper = objectMapper;
this.auditService = auditService;
this.diagramStore = diagramStore;
}
@GetMapping
@Operation(summary = "List all application configs",
description = "Returns stored configurations for all applications")
@ApiResponse(responseCode = "200", description = "Configs returned")
public ResponseEntity<List<ApplicationConfig>> listConfigs(HttpServletRequest httpRequest) {
auditService.log("view_app_configs", AuditCategory.CONFIG, null, null, AuditResult.SUCCESS, httpRequest);
return ResponseEntity.ok(configRepository.findAll());
}
@GetMapping("/{application}")
@Operation(summary = "Get application config",
description = "Returns the current configuration for an application. Returns defaults if none stored.")
@ApiResponse(responseCode = "200", description = "Config returned")
public ResponseEntity<ApplicationConfig> getConfig(@PathVariable String application,
HttpServletRequest httpRequest) {
auditService.log("view_app_config", AuditCategory.CONFIG, application, null, AuditResult.SUCCESS, httpRequest);
return ResponseEntity.ok(
configRepository.findByApplication(application)
.orElse(defaultConfig(application)));
}
@PutMapping("/{application}")
@Operation(summary = "Update application config",
description = "Saves config and pushes CONFIG_UPDATE to all LIVE agents of this application")
@ApiResponse(responseCode = "200", description = "Config saved and pushed")
public ResponseEntity<ApplicationConfig> updateConfig(@PathVariable String application,
@RequestBody ApplicationConfig config,
Authentication auth,
HttpServletRequest httpRequest) {
String updatedBy = auth != null ? auth.getName() : "system";
config.setApplication(application);
ApplicationConfig saved = configRepository.save(application, config, updatedBy);
int pushed = pushConfigToAgents(application, saved);
log.info("Config v{} saved for '{}', pushed to {} agent(s)", saved.getVersion(), application, pushed);
auditService.log("update_app_config", AuditCategory.CONFIG, application,
Map.of("version", saved.getVersion(), "agentsPushed", pushed),
AuditResult.SUCCESS, httpRequest);
return ResponseEntity.ok(saved);
}
@GetMapping("/{application}/processor-routes")
@Operation(summary = "Get processor to route mapping",
description = "Returns a map of processorId → routeId for all processors seen in this application")
@ApiResponse(responseCode = "200", description = "Mapping returned")
public ResponseEntity<Map<String, String>> getProcessorRouteMapping(@PathVariable String application) {
return ResponseEntity.ok(diagramStore.findProcessorRouteMapping(application));
}
@PostMapping("/{application}/test-expression")
@Operation(summary = "Test a tap expression against sample data via a live agent")
@ApiResponse(responseCode = "200", description = "Expression evaluated successfully")
@ApiResponse(responseCode = "404", description = "No live agent available for this application")
@ApiResponse(responseCode = "504", description = "Agent did not respond in time")
public ResponseEntity<TestExpressionResponse> testExpression(
@PathVariable String application,
@RequestBody TestExpressionRequest request) {
// Find a LIVE agent for this application
AgentInfo agent = registryService.findAll().stream()
.filter(a -> application.equals(a.application()))
.filter(a -> a.state() == AgentState.LIVE)
.findFirst()
.orElse(null);
if (agent == null) {
return ResponseEntity.status(HttpStatus.NOT_FOUND)
.body(new TestExpressionResponse(null, "No live agent available for application: " + application));
}
// Build payload JSON
String payloadJson;
try {
payloadJson = objectMapper.writeValueAsString(Map.of(
"expression", request.expression() != null ? request.expression() : "",
"language", request.language() != null ? request.language() : "",
"body", request.body() != null ? request.body() : "",
"target", request.target() != null ? request.target() : ""
));
} catch (JsonProcessingException e) {
log.error("Failed to serialize test-expression payload", e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(new TestExpressionResponse(null, "Failed to serialize request"));
}
// Send command and await reply
CompletableFuture<CommandReply> future = registryService.addCommandWithReply(
agent.id(), CommandType.TEST_EXPRESSION, payloadJson);
try {
CommandReply reply = future.orTimeout(5, TimeUnit.SECONDS).join();
if ("SUCCESS".equals(reply.status())) {
return ResponseEntity.ok(new TestExpressionResponse(reply.data(), null));
} else {
return ResponseEntity.ok(new TestExpressionResponse(null, reply.message()));
}
} catch (CompletionException e) {
if (e.getCause() instanceof TimeoutException) {
return ResponseEntity.status(HttpStatus.GATEWAY_TIMEOUT)
.body(new TestExpressionResponse(null, "Agent did not respond within 5 seconds"));
}
log.error("Error awaiting test-expression reply from agent {}", agent.id(), e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(new TestExpressionResponse(null, "Internal error: " + e.getCause().getMessage()));
}
}
private int pushConfigToAgents(String application, ApplicationConfig config) {
String payloadJson;
try {
payloadJson = objectMapper.writeValueAsString(config);
} catch (JsonProcessingException e) {
log.error("Failed to serialize config for push", e);
return 0;
}
List<AgentInfo> agents = registryService.findAll().stream()
.filter(a -> a.state() == AgentState.LIVE)
.filter(a -> application.equals(a.application()))
.toList();
for (AgentInfo agent : agents) {
registryService.addCommand(agent.id(), CommandType.CONFIG_UPDATE, payloadJson);
}
return agents.size();
}
private static ApplicationConfig defaultConfig(String application) {
ApplicationConfig config = new ApplicationConfig();
config.setApplication(application);
config.setVersion(0);
config.setMetricsEnabled(true);
config.setSamplingRate(1.0);
config.setTracedProcessors(Map.of());
config.setApplicationLogLevel("INFO");
config.setAgentLogLevel("INFO");
config.setEngineLevel("REGULAR");
config.setPayloadCaptureMode("NONE");
return config;
}
}

View File

@@ -5,8 +5,11 @@ import com.cameleer3.server.core.admin.AuditCategory;
import com.cameleer3.server.core.admin.AuditRepository; import com.cameleer3.server.core.admin.AuditRepository;
import com.cameleer3.server.core.admin.AuditRepository.AuditPage; import com.cameleer3.server.core.admin.AuditRepository.AuditPage;
import com.cameleer3.server.core.admin.AuditRepository.AuditQuery; import com.cameleer3.server.core.admin.AuditRepository.AuditQuery;
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.Operation;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.format.annotation.DateTimeFormat; import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.access.prepost.PreAuthorize;
@@ -16,8 +19,6 @@ import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import java.time.Instant; import java.time.Instant;
import java.time.LocalDate;
import java.time.ZoneOffset;
@RestController @RestController
@RequestMapping("/api/v1/admin/audit") @RequestMapping("/api/v1/admin/audit")
@@ -26,19 +27,22 @@ import java.time.ZoneOffset;
public class AuditLogController { public class AuditLogController {
private final AuditRepository auditRepository; private final AuditRepository auditRepository;
private final AuditService auditService;
public AuditLogController(AuditRepository auditRepository) { public AuditLogController(AuditRepository auditRepository, AuditService auditService) {
this.auditRepository = auditRepository; this.auditRepository = auditRepository;
this.auditService = auditService;
} }
@GetMapping @GetMapping
@Operation(summary = "Search audit log entries with pagination") @Operation(summary = "Search audit log entries with pagination")
public ResponseEntity<AuditLogPageResponse> getAuditLog( public ResponseEntity<AuditLogPageResponse> getAuditLog(
HttpServletRequest httpRequest,
@RequestParam(required = false) String username, @RequestParam(required = false) String username,
@RequestParam(required = false) String category, @RequestParam(required = false) String category,
@RequestParam(required = false) String search, @RequestParam(required = false) String search,
@RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate from, @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) Instant from,
@RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate to, @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) Instant to,
@RequestParam(defaultValue = "timestamp") String sort, @RequestParam(defaultValue = "timestamp") String sort,
@RequestParam(defaultValue = "desc") String order, @RequestParam(defaultValue = "desc") String order,
@RequestParam(defaultValue = "0") int page, @RequestParam(defaultValue = "0") int page,
@@ -46,8 +50,8 @@ public class AuditLogController {
size = Math.min(size, 100); size = Math.min(size, 100);
Instant fromInstant = from != null ? from.atStartOfDay(ZoneOffset.UTC).toInstant() : null; Instant fromInstant = from != null ? from : Instant.now().minus(java.time.Duration.ofDays(7));
Instant toInstant = to != null ? to.plusDays(1).atStartOfDay(ZoneOffset.UTC).toInstant() : null; Instant toInstant = to != null ? to : Instant.now();
AuditCategory cat = null; AuditCategory cat = null;
if (category != null && !category.isEmpty()) { if (category != null && !category.isEmpty()) {
@@ -58,6 +62,8 @@ public class AuditLogController {
} }
} }
auditService.log("view_audit_log", AuditCategory.AUTH, null, null, AuditResult.SUCCESS, httpRequest);
AuditQuery query = new AuditQuery(username, cat, search, fromInstant, toInstant, sort, order, page, size); AuditQuery query = new AuditQuery(username, cat, search, fromInstant, toInstant, sort, order, page, size);
AuditPage result = auditRepository.find(query); AuditPage result = auditRepository.find(query);

View File

@@ -7,6 +7,7 @@ import com.cameleer3.server.app.dto.TableSizeResponse;
import com.cameleer3.server.core.admin.AuditCategory; import com.cameleer3.server.core.admin.AuditCategory;
import com.cameleer3.server.core.admin.AuditResult; import com.cameleer3.server.core.admin.AuditResult;
import com.cameleer3.server.core.admin.AuditService; import com.cameleer3.server.core.admin.AuditService;
import com.cameleer3.server.core.ingestion.IngestionService;
import com.zaxxer.hikari.HikariDataSource; import com.zaxxer.hikari.HikariDataSource;
import com.zaxxer.hikari.HikariPoolMXBean; import com.zaxxer.hikari.HikariPoolMXBean;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
@@ -24,7 +25,9 @@ import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.server.ResponseStatusException; import org.springframework.web.server.ResponseStatusException;
import javax.sql.DataSource; import javax.sql.DataSource;
import java.time.Instant;
import java.util.List; import java.util.List;
import java.util.Map;
@RestController @RestController
@RequestMapping("/api/v1/admin/database") @RequestMapping("/api/v1/admin/database")
@@ -35,11 +38,14 @@ public class DatabaseAdminController {
private final JdbcTemplate jdbc; private final JdbcTemplate jdbc;
private final DataSource dataSource; private final DataSource dataSource;
private final AuditService auditService; private final AuditService auditService;
private final IngestionService ingestionService;
public DatabaseAdminController(JdbcTemplate jdbc, DataSource dataSource, AuditService auditService) { public DatabaseAdminController(JdbcTemplate jdbc, DataSource dataSource,
AuditService auditService, IngestionService ingestionService) {
this.jdbc = jdbc; this.jdbc = jdbc;
this.dataSource = dataSource; this.dataSource = dataSource;
this.auditService = auditService; this.auditService = auditService;
this.ingestionService = ingestionService;
} }
@GetMapping("/status") @GetMapping("/status")
@@ -53,7 +59,8 @@ public class DatabaseAdminController {
String host = extractHost(dataSource); String host = extractHost(dataSource);
return ResponseEntity.ok(new DatabaseStatusResponse(true, version, host, schema, timescaleDb)); return ResponseEntity.ok(new DatabaseStatusResponse(true, version, host, schema, timescaleDb));
} catch (Exception e) { } 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));
} }
} }
@@ -117,6 +124,29 @@ public class DatabaseAdminController {
return ResponseEntity.ok().build(); return ResponseEntity.ok().build();
} }
@GetMapping("/metrics-pipeline")
@Operation(summary = "Get metrics ingestion pipeline diagnostics")
public ResponseEntity<Map<String, Object>> getMetricsPipeline() {
int bufferDepth = ingestionService.getMetricsBufferDepth();
Long totalRows = jdbc.queryForObject(
"SELECT count(*) FROM agent_metrics", Long.class);
List<String> agentIds = jdbc.queryForList(
"SELECT DISTINCT agent_id FROM agent_metrics ORDER BY agent_id", String.class);
Instant latestCollected = jdbc.queryForObject(
"SELECT max(collected_at) FROM agent_metrics", Instant.class);
List<String> metricNames = jdbc.queryForList(
"SELECT DISTINCT metric_name FROM agent_metrics ORDER BY metric_name", String.class);
return ResponseEntity.ok(Map.of(
"bufferDepth", bufferDepth,
"totalRows", totalRows != null ? totalRows : 0,
"distinctAgents", agentIds,
"distinctMetrics", metricNames,
"latestCollectedAt", latestCollected != null ? latestCollected.toString() : "none"
));
}
private String extractHost(DataSource ds) { private String extractHost(DataSource ds) {
try { try {
if (ds instanceof HikariDataSource hds) { if (ds instanceof HikariDataSource hds) {

View File

@@ -49,7 +49,7 @@ public class DetailController {
} }
@GetMapping("/{executionId}/processors/{index}/snapshot") @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 = "200", description = "Snapshot data")
@ApiResponse(responseCode = "404", description = "Snapshot not found") @ApiResponse(responseCode = "404", description = "Snapshot not found")
public ResponseEntity<Map<String, String>> getProcessorSnapshot( public ResponseEntity<Map<String, String>> getProcessorSnapshot(
@@ -69,4 +69,16 @@ public class DetailController {
return ResponseEntity.ok(snapshot); 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

@@ -1,6 +1,8 @@
package com.cameleer3.server.app.controller; package com.cameleer3.server.app.controller;
import com.cameleer3.common.graph.RouteGraph; 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.ingestion.IngestionService; import com.cameleer3.server.core.ingestion.IngestionService;
import com.cameleer3.server.core.ingestion.TaggedDiagram; import com.cameleer3.server.core.ingestion.TaggedDiagram;
import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.JsonProcessingException;
@@ -35,10 +37,14 @@ public class DiagramController {
private static final Logger log = LoggerFactory.getLogger(DiagramController.class); private static final Logger log = LoggerFactory.getLogger(DiagramController.class);
private final IngestionService ingestionService; private final IngestionService ingestionService;
private final AgentRegistryService registryService;
private final ObjectMapper objectMapper; private final ObjectMapper objectMapper;
public DiagramController(IngestionService ingestionService, ObjectMapper objectMapper) { public DiagramController(IngestionService ingestionService,
AgentRegistryService registryService,
ObjectMapper objectMapper) {
this.ingestionService = ingestionService; this.ingestionService = ingestionService;
this.registryService = registryService;
this.objectMapper = objectMapper; this.objectMapper = objectMapper;
} }
@@ -48,10 +54,11 @@ public class DiagramController {
@ApiResponse(responseCode = "202", description = "Data accepted for processing") @ApiResponse(responseCode = "202", description = "Data accepted for processing")
public ResponseEntity<Void> ingestDiagrams(@RequestBody String body) throws JsonProcessingException { public ResponseEntity<Void> ingestDiagrams(@RequestBody String body) throws JsonProcessingException {
String agentId = extractAgentId(); String agentId = extractAgentId();
String applicationName = resolveApplicationName(agentId);
List<RouteGraph> graphs = parsePayload(body); List<RouteGraph> graphs = parsePayload(body);
for (RouteGraph graph : graphs) { for (RouteGraph graph : graphs) {
ingestionService.ingestDiagram(new TaggedDiagram(agentId, graph)); ingestionService.ingestDiagram(new TaggedDiagram(agentId, applicationName, graph));
} }
return ResponseEntity.accepted().build(); return ResponseEntity.accepted().build();
@@ -62,6 +69,11 @@ public class DiagramController {
return auth != null ? auth.getName() : ""; return auth != null ? auth.getName() : "";
} }
private String resolveApplicationName(String agentId) {
AgentInfo agent = registryService.findById(agentId);
return agent != null ? agent.application() : "";
}
private List<RouteGraph> parsePayload(String body) throws JsonProcessingException { private List<RouteGraph> parsePayload(String body) throws JsonProcessingException {
String trimmed = body.strip(); String trimmed = body.strip();
if (trimmed.startsWith("[")) { if (trimmed.startsWith("[")) {

View File

@@ -62,6 +62,7 @@ public class DiagramRenderController {
@ApiResponse(responseCode = "404", description = "Diagram not found") @ApiResponse(responseCode = "404", description = "Diagram not found")
public ResponseEntity<?> renderDiagram( public ResponseEntity<?> renderDiagram(
@PathVariable String contentHash, @PathVariable String contentHash,
@RequestParam(defaultValue = "LR") String direction,
HttpServletRequest request) { HttpServletRequest request) {
Optional<RouteGraph> graphOpt = diagramStore.findByContentHash(contentHash); Optional<RouteGraph> graphOpt = diagramStore.findByContentHash(contentHash);
@@ -76,7 +77,7 @@ public class DiagramRenderController {
// without also accepting everything (*/*). This means "application/json" // without also accepting everything (*/*). This means "application/json"
// must appear and wildcards must not dominate the preference. // must appear and wildcards must not dominate the preference.
if (accept != null && isJsonPreferred(accept)) { if (accept != null && isJsonPreferred(accept)) {
DiagramLayout layout = diagramRenderer.layoutJson(graph); DiagramLayout layout = diagramRenderer.layoutJson(graph, direction);
return ResponseEntity.ok() return ResponseEntity.ok()
.contentType(MediaType.APPLICATION_JSON) .contentType(MediaType.APPLICATION_JSON)
.body(layout); .body(layout);
@@ -96,7 +97,8 @@ public class DiagramRenderController {
@ApiResponse(responseCode = "404", description = "No diagram found for the given application and route") @ApiResponse(responseCode = "404", description = "No diagram found for the given application and route")
public ResponseEntity<DiagramLayout> findByApplicationAndRoute( public ResponseEntity<DiagramLayout> findByApplicationAndRoute(
@RequestParam String application, @RequestParam String application,
@RequestParam String routeId) { @RequestParam String routeId,
@RequestParam(defaultValue = "LR") String direction) {
List<String> agentIds = registryService.findByApplication(application).stream() List<String> agentIds = registryService.findByApplication(application).stream()
.map(AgentInfo::id) .map(AgentInfo::id)
.toList(); .toList();
@@ -115,7 +117,7 @@ public class DiagramRenderController {
return ResponseEntity.notFound().build(); return ResponseEntity.notFound().build();
} }
DiagramLayout layout = diagramRenderer.layoutJson(graphOpt.get()); DiagramLayout layout = diagramRenderer.layoutJson(graphOpt.get(), direction);
return ResponseEntity.ok(layout); return ResponseEntity.ok(layout);
} }

View File

@@ -0,0 +1,61 @@
package com.cameleer3.server.app.controller;
import com.cameleer3.common.model.LogBatch;
import com.cameleer3.server.app.search.OpenSearchLogIndex;
import com.cameleer3.server.core.agent.AgentInfo;
import com.cameleer3.server.core.agent.AgentRegistryService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api/v1/data")
@Tag(name = "Ingestion", description = "Data ingestion endpoints")
public class LogIngestionController {
private static final Logger log = LoggerFactory.getLogger(LogIngestionController.class);
private final OpenSearchLogIndex logIndex;
private final AgentRegistryService registryService;
public LogIngestionController(OpenSearchLogIndex logIndex,
AgentRegistryService registryService) {
this.logIndex = logIndex;
this.registryService = registryService;
}
@PostMapping("/logs")
@Operation(summary = "Ingest application log entries",
description = "Accepts a batch of log entries from an agent. Entries are indexed in OpenSearch.")
@ApiResponse(responseCode = "202", description = "Logs accepted for indexing")
public ResponseEntity<Void> ingestLogs(@RequestBody LogBatch batch) {
String agentId = extractAgentId();
String application = resolveApplicationName(agentId);
if (batch.getEntries() != null && !batch.getEntries().isEmpty()) {
log.debug("Received {} log entries from agent={}, app={}", batch.getEntries().size(), agentId, application);
logIndex.indexBatch(agentId, application, batch.getEntries());
}
return ResponseEntity.accepted().build();
}
private String extractAgentId() {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
return auth != null ? auth.getName() : "";
}
private String resolveApplicationName(String agentId) {
AgentInfo agent = registryService.findById(agentId);
return agent != null ? agent.application() : "";
}
}

View File

@@ -0,0 +1,50 @@
package com.cameleer3.server.app.controller;
import com.cameleer3.server.app.dto.LogEntryResponse;
import com.cameleer3.server.app.search.OpenSearchLogIndex;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.time.Instant;
import java.util.List;
@RestController
@RequestMapping("/api/v1/logs")
@Tag(name = "Application Logs", description = "Query application logs stored in OpenSearch")
public class LogQueryController {
private final OpenSearchLogIndex logIndex;
public LogQueryController(OpenSearchLogIndex logIndex) {
this.logIndex = logIndex;
}
@GetMapping
@Operation(summary = "Search application log entries",
description = "Returns log entries for a given application, optionally filtered by agent, level, time range, and text query")
public ResponseEntity<List<LogEntryResponse>> searchLogs(
@RequestParam String application,
@RequestParam(required = false) String agentId,
@RequestParam(required = false) String level,
@RequestParam(required = false) String query,
@RequestParam(required = false) String exchangeId,
@RequestParam(required = false) String from,
@RequestParam(required = false) String to,
@RequestParam(defaultValue = "200") int limit) {
limit = Math.min(limit, 1000);
Instant fromInstant = from != null ? Instant.parse(from) : null;
Instant toInstant = to != null ? Instant.parse(to) : null;
List<LogEntryResponse> entries = logIndex.search(
application, agentId, level, query, exchangeId, fromInstant, toInstant, limit);
return ResponseEntity.ok(entries);
}
}

View File

@@ -44,13 +44,23 @@ public class MetricsController {
@Operation(summary = "Ingest agent metrics", @Operation(summary = "Ingest agent metrics",
description = "Accepts an array of MetricsSnapshot objects") description = "Accepts an array of MetricsSnapshot objects")
@ApiResponse(responseCode = "202", description = "Data accepted for processing") @ApiResponse(responseCode = "202", description = "Data accepted for processing")
@ApiResponse(responseCode = "400", description = "Invalid payload")
@ApiResponse(responseCode = "503", description = "Buffer full, retry later") @ApiResponse(responseCode = "503", description = "Buffer full, retry later")
public ResponseEntity<Void> ingestMetrics(@RequestBody String body) throws JsonProcessingException { public ResponseEntity<Void> ingestMetrics(@RequestBody String body) {
List<MetricsSnapshot> metrics = parsePayload(body); List<MetricsSnapshot> metrics;
boolean accepted = ingestionService.acceptMetrics(metrics); try {
metrics = parsePayload(body);
} catch (JsonProcessingException e) {
log.warn("Failed to parse metrics payload: {}", e.getMessage());
return ResponseEntity.badRequest().build();
}
log.debug("Received {} metric(s) from agent(s)", metrics.size());
boolean accepted = ingestionService.acceptMetrics(metrics);
if (!accepted) { if (!accepted) {
log.warn("Metrics buffer full, returning 503"); log.warn("Metrics buffer full ({} items), returning 503",
ingestionService.getMetricsBufferDepth());
return ResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE) return ResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE)
.header("Retry-After", "5") .header("Retry-After", "5")
.build(); .build();

View File

@@ -61,7 +61,8 @@ public class OidcConfigAdminController {
@GetMapping @GetMapping
@Operation(summary = "Get OIDC configuration") @Operation(summary = "Get OIDC configuration")
@ApiResponse(responseCode = "200", description = "Current OIDC configuration (client_secret masked)") @ApiResponse(responseCode = "200", description = "Current OIDC configuration (client_secret masked)")
public ResponseEntity<OidcAdminConfigResponse> getConfig() { public ResponseEntity<OidcAdminConfigResponse> getConfig(HttpServletRequest httpRequest) {
auditService.log("view_oidc_config", AuditCategory.CONFIG, null, null, AuditResult.SUCCESS, httpRequest);
Optional<OidcConfig> config = configRepository.find(); Optional<OidcConfig> config = configRepository.find();
if (config.isEmpty()) { if (config.isEmpty()) {
return ResponseEntity.ok(OidcAdminConfigResponse.unconfigured()); return ResponseEntity.ok(OidcAdminConfigResponse.unconfigured());

View File

@@ -49,12 +49,14 @@ public class OpenSearchAdminController {
private final ObjectMapper objectMapper; private final ObjectMapper objectMapper;
private final String opensearchUrl; private final String opensearchUrl;
private final String indexPrefix; private final String indexPrefix;
private final String logIndexPrefix;
public OpenSearchAdminController(OpenSearchClient client, RestClient restClient, public OpenSearchAdminController(OpenSearchClient client, RestClient restClient,
SearchIndexerStats indexerStats, AuditService auditService, SearchIndexerStats indexerStats, AuditService auditService,
ObjectMapper objectMapper, ObjectMapper objectMapper,
@Value("${opensearch.url:http://localhost:9200}") String opensearchUrl, @Value("${opensearch.url:http://localhost:9200}") String opensearchUrl,
@Value("${opensearch.index-prefix:executions-}") String indexPrefix) { @Value("${opensearch.index-prefix:executions-}") String indexPrefix,
@Value("${opensearch.log-index-prefix:logs-}") String logIndexPrefix) {
this.client = client; this.client = client;
this.restClient = restClient; this.restClient = restClient;
this.indexerStats = indexerStats; this.indexerStats = indexerStats;
@@ -62,6 +64,7 @@ public class OpenSearchAdminController {
this.objectMapper = objectMapper; this.objectMapper = objectMapper;
this.opensearchUrl = opensearchUrl; this.opensearchUrl = opensearchUrl;
this.indexPrefix = indexPrefix; this.indexPrefix = indexPrefix;
this.logIndexPrefix = logIndexPrefix;
} }
@GetMapping("/status") @GetMapping("/status")
@@ -77,7 +80,8 @@ public class OpenSearchAdminController {
health.numberOfNodes(), health.numberOfNodes(),
opensearchUrl)); opensearchUrl));
} catch (Exception e) { } catch (Exception e) {
return ResponseEntity.ok(new OpenSearchStatusResponse( return ResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE)
.body(new OpenSearchStatusResponse(
false, "UNREACHABLE", null, 0, opensearchUrl)); false, "UNREACHABLE", null, 0, opensearchUrl));
} }
} }
@@ -100,7 +104,8 @@ public class OpenSearchAdminController {
public ResponseEntity<IndicesPageResponse> getIndices( public ResponseEntity<IndicesPageResponse> getIndices(
@RequestParam(defaultValue = "0") int page, @RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "20") int size, @RequestParam(defaultValue = "20") int size,
@RequestParam(defaultValue = "") String search) { @RequestParam(defaultValue = "") String search,
@RequestParam(defaultValue = "executions") String prefix) {
try { try {
Response response = restClient.performRequest( Response response = restClient.performRequest(
new Request("GET", "/_cat/indices?format=json&h=index,health,docs.count,store.size,pri,rep&bytes=b")); new Request("GET", "/_cat/indices?format=json&h=index,health,docs.count,store.size,pri,rep&bytes=b"));
@@ -109,10 +114,12 @@ public class OpenSearchAdminController {
indices = objectMapper.readTree(is); indices = objectMapper.readTree(is);
} }
String filterPrefix = "logs".equals(prefix) ? logIndexPrefix : indexPrefix;
List<IndexInfoResponse> allIndices = new ArrayList<>(); List<IndexInfoResponse> allIndices = new ArrayList<>();
for (JsonNode idx : indices) { for (JsonNode idx : indices) {
String name = idx.path("index").asText(""); String name = idx.path("index").asText("");
if (!name.startsWith(indexPrefix)) { if (!name.startsWith(filterPrefix)) {
continue; continue;
} }
if (!search.isEmpty() && !name.contains(search)) { if (!search.isEmpty() && !name.contains(search)) {
@@ -143,7 +150,8 @@ public class OpenSearchAdminController {
pageItems, totalIndices, totalDocs, pageItems, totalIndices, totalDocs,
humanSize(totalBytes), page, size, totalPages)); humanSize(totalBytes), page, size, totalPages));
} catch (Exception e) { } 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)); List.of(), 0, 0, "0 B", page, size, 0));
} }
} }
@@ -152,7 +160,7 @@ public class OpenSearchAdminController {
@Operation(summary = "Delete an OpenSearch index") @Operation(summary = "Delete an OpenSearch index")
public ResponseEntity<Void> deleteIndex(@PathVariable String name, HttpServletRequest request) { public ResponseEntity<Void> deleteIndex(@PathVariable String name, HttpServletRequest request) {
try { try {
if (!name.startsWith(indexPrefix)) { if (!name.startsWith(indexPrefix) && !name.startsWith(logIndexPrefix)) {
throw new ResponseStatusException(HttpStatus.FORBIDDEN, "Cannot delete index outside application scope"); throw new ResponseStatusException(HttpStatus.FORBIDDEN, "Cannot delete index outside application scope");
} }
boolean exists = client.indices().exists(r -> r.index(name)).value(); boolean exists = client.indices().exists(r -> r.index(name)).value();
@@ -228,7 +236,8 @@ public class OpenSearchAdminController {
searchLatency, indexingLatency, searchLatency, indexingLatency,
heapUsed, heapMax)); heapUsed, heapMax));
} catch (Exception e) { } 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.AgentSummary;
import com.cameleer3.server.app.dto.AppCatalogEntry; import com.cameleer3.server.app.dto.AppCatalogEntry;
import com.cameleer3.server.app.dto.RouteSummary; 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.AgentInfo;
import com.cameleer3.server.core.agent.AgentRegistryService; import com.cameleer3.server.core.agent.AgentRegistryService;
import com.cameleer3.server.core.agent.AgentState; import com.cameleer3.server.core.agent.AgentState;
import com.cameleer3.server.core.storage.DiagramStore;
import com.cameleer3.server.core.storage.StatsStore; import com.cameleer3.server.core.storage.StatsStore;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponse;
@@ -14,6 +16,7 @@ import org.springframework.http.ResponseEntity;
import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import java.sql.Timestamp; import java.sql.Timestamp;
@@ -33,10 +36,14 @@ import java.util.stream.Collectors;
public class RouteCatalogController { public class RouteCatalogController {
private final AgentRegistryService registryService; private final AgentRegistryService registryService;
private final DiagramStore diagramStore;
private final JdbcTemplate jdbc; private final JdbcTemplate jdbc;
public RouteCatalogController(AgentRegistryService registryService, JdbcTemplate jdbc) { public RouteCatalogController(AgentRegistryService registryService,
DiagramStore diagramStore,
JdbcTemplate jdbc) {
this.registryService = registryService; this.registryService = registryService;
this.diagramStore = diagramStore;
this.jdbc = jdbc; this.jdbc = jdbc;
} }
@@ -44,7 +51,9 @@ public class RouteCatalogController {
@Operation(summary = "Get route catalog", @Operation(summary = "Get route catalog",
description = "Returns all applications with their routes, agents, and health status") description = "Returns all applications with their routes, agents, and health status")
@ApiResponse(responseCode = "200", description = "Catalog returned") @ApiResponse(responseCode = "200", description = "Catalog returned")
public ResponseEntity<List<AppCatalogEntry>> getCatalog() { public ResponseEntity<List<AppCatalogEntry>> getCatalog(
@RequestParam(required = false) String from,
@RequestParam(required = false) String to) {
List<AgentInfo> allAgents = registryService.findAll(); List<AgentInfo> allAgents = registryService.findAll();
// Group agents by application name // Group agents by application name
@@ -63,9 +72,10 @@ public class RouteCatalogController {
routesByApp.put(entry.getKey(), routes); routesByApp.put(entry.getKey(), routes);
} }
// Query route-level stats for the last 24 hours // Time range for exchange counts — use provided range or default to last 24h
Instant now = Instant.now(); Instant now = Instant.now();
Instant from24h = now.minus(24, ChronoUnit.HOURS); Instant rangeFrom = from != null ? Instant.parse(from) : now.minus(24, ChronoUnit.HOURS);
Instant rangeTo = to != null ? Instant.parse(to) : now;
Instant from1m = now.minus(1, ChronoUnit.MINUTES); Instant from1m = now.minus(1, ChronoUnit.MINUTES);
// Route exchange counts from continuous aggregate // Route exchange counts from continuous aggregate
@@ -82,7 +92,7 @@ public class RouteCatalogController {
Timestamp ts = rs.getTimestamp("last_seen"); Timestamp ts = rs.getTimestamp("last_seen");
if (ts != null) routeLastSeen.put(key, ts.toInstant()); if (ts != null) routeLastSeen.put(key, ts.toInstant());
}, },
Timestamp.from(from24h), Timestamp.from(now)); Timestamp.from(rangeFrom), Timestamp.from(rangeTo));
} catch (Exception e) { } catch (Exception e) {
// Continuous aggregate may not exist yet // Continuous aggregate may not exist yet
} }
@@ -110,12 +120,14 @@ public class RouteCatalogController {
// Routes // Routes
Set<String> routeIds = routesByApp.getOrDefault(appId, Set.of()); Set<String> routeIds = routesByApp.getOrDefault(appId, Set.of());
List<String> agentIds = agents.stream().map(AgentInfo::id).toList();
List<RouteSummary> routeSummaries = routeIds.stream() List<RouteSummary> routeSummaries = routeIds.stream()
.map(routeId -> { .map(routeId -> {
String key = appId + "/" + routeId; String key = appId + "/" + routeId;
long count = routeExchangeCounts.getOrDefault(key, 0L); long count = routeExchangeCounts.getOrDefault(key, 0L);
Instant lastSeen = routeLastSeen.get(key); Instant lastSeen = routeLastSeen.get(key);
return new RouteSummary(routeId, count, lastSeen); String fromUri = resolveFromEndpointUri(routeId, agentIds);
return new RouteSummary(routeId, count, lastSeen, fromUri);
}) })
.toList(); .toList();
@@ -137,6 +149,15 @@ public class RouteCatalogController {
return ResponseEntity.ok(catalog); 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) { private String computeWorstHealth(List<AgentInfo> agents) {
boolean hasDead = false; boolean hasDead = false;
boolean hasStale = 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.ProcessorMetrics;
import com.cameleer3.server.app.dto.RouteMetrics; 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.Operation;
import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
@@ -18,6 +21,7 @@ import java.time.Instant;
import java.time.temporal.ChronoUnit; import java.time.temporal.ChronoUnit;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Map;
@RestController @RestController
@RequestMapping("/api/v1/routes") @RequestMapping("/api/v1/routes")
@@ -25,9 +29,14 @@ import java.util.List;
public class RouteMetricsController { public class RouteMetricsController {
private final JdbcTemplate jdbc; 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.jdbc = jdbc;
this.statsStore = statsStore;
this.appSettingsRepository = appSettingsRepository;
} }
@GetMapping("/metrics") @GetMapping("/metrics")
@@ -78,7 +87,7 @@ public class RouteMetricsController {
routeKeys.add(new RouteKey(applicationName, routeId)); routeKeys.add(new RouteKey(applicationName, routeId));
return new RouteMetrics(routeId, applicationName, total, successRate, return new RouteMetrics(routeId, applicationName, total, successRate,
avgDur, p99Dur, errorRate, tps, List.of()); avgDur, p99Dur, errorRate, tps, List.of(), -1.0);
}, params.toArray()); }, params.toArray());
// Fetch sparklines (12 buckets over the time window) // Fetch sparklines (12 buckets over the time window)
@@ -100,13 +109,34 @@ public class RouteMetricsController {
m.appId(), m.routeId()); m.appId(), m.routeId());
metrics.set(i, new RouteMetrics(m.routeId(), m.appId(), m.exchangeCount(), metrics.set(i, new RouteMetrics(m.routeId(), m.appId(), m.exchangeCount(),
m.successRate(), m.avgDurationMs(), m.p99DurationMs(), m.successRate(), m.avgDurationMs(), m.p99DurationMs(),
m.errorRate(), m.throughputPerSec(), sparkline)); m.errorRate(), m.throughputPerSec(), sparkline, m.slaCompliance()));
} catch (Exception e) { } catch (Exception e) {
// Leave sparkline empty on error // 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); return ResponseEntity.ok(metrics);
} }

View File

@@ -1,5 +1,7 @@
package com.cameleer3.server.app.controller; 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.AgentInfo;
import com.cameleer3.server.core.agent.AgentRegistryService; import com.cameleer3.server.core.agent.AgentRegistryService;
import com.cameleer3.server.core.search.ExecutionStats; 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.SearchResult;
import com.cameleer3.server.core.search.SearchService; import com.cameleer3.server.core.search.SearchService;
import com.cameleer3.server.core.search.StatsTimeseries; 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.Operation;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
@@ -20,6 +24,7 @@ import org.springframework.web.bind.annotation.RestController;
import java.time.Instant; import java.time.Instant;
import java.util.List; import java.util.List;
import java.util.Map;
/** /**
* Search endpoints for querying route executions. * Search endpoints for querying route executions.
@@ -34,10 +39,13 @@ public class SearchController {
private final SearchService searchService; private final SearchService searchService;
private final AgentRegistryService registryService; 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.searchService = searchService;
this.registryService = registryService; this.registryService = registryService;
this.appSettingsRepository = appSettingsRepository;
} }
@GetMapping("/executions") @GetMapping("/executions")
@@ -87,21 +95,29 @@ public class SearchController {
} }
@GetMapping("/stats") @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( public ResponseEntity<ExecutionStats> stats(
@RequestParam Instant from, @RequestParam Instant from,
@RequestParam(required = false) Instant to, @RequestParam(required = false) Instant to,
@RequestParam(required = false) String routeId, @RequestParam(required = false) String routeId,
@RequestParam(required = false) String application) { @RequestParam(required = false) String application) {
Instant end = to != null ? to : Instant.now(); Instant end = to != null ? to : Instant.now();
ExecutionStats stats;
if (routeId == null && application == null) { if (routeId == null && application == null) {
return ResponseEntity.ok(searchService.stats(from, end)); stats = searchService.stats(from, end);
} } else if (routeId == null) {
if (routeId == null) { stats = searchService.statsForApp(from, end, application);
return ResponseEntity.ok(searchService.statsForApp(from, end, application)); } else {
}
List<String> agentIds = resolveApplicationToAgentIds(application); List<String> agentIds = resolveApplicationToAgentIds(application);
return ResponseEntity.ok(searchService.stats(from, end, routeId, agentIds)); stats = 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") @GetMapping("/stats/timeseries")
@@ -126,6 +142,48 @@ public class SearchController {
return ResponseEntity.ok(searchService.timeseries(from, end, buckets, routeId, agentIds)); 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. * Resolve an application name to agent IDs.
* Returns null if application is null/blank (no filtering). * Returns null if application is null/blank (no filtering).

View File

@@ -58,7 +58,8 @@ public class UserAdminController {
@GetMapping @GetMapping
@Operation(summary = "List all users with RBAC detail") @Operation(summary = "List all users with RBAC detail")
@ApiResponse(responseCode = "200", description = "User list returned") @ApiResponse(responseCode = "200", description = "User list returned")
public ResponseEntity<List<UserDetail>> listUsers() { public ResponseEntity<List<UserDetail>> listUsers(HttpServletRequest httpRequest) {
auditService.log("view_users", AuditCategory.USER_MGMT, null, null, AuditResult.SUCCESS, httpRequest);
return ResponseEntity.ok(rbacService.listUsers()); return ResponseEntity.ok(rbacService.listUsers());
} }

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,11 @@
package com.cameleer3.server.app.dto;
/**
* Request body for command acknowledgment from agents.
* Contains the result status and message of the command execution.
*
* @param status "SUCCESS" or "FAILURE"
* @param message human-readable description of the result
* @param data optional structured JSON data returned by the agent (e.g. expression evaluation results)
*/
public record CommandAckRequest(String status, String message, String data) {}

View File

@@ -0,0 +1,13 @@
package com.cameleer3.server.app.dto;
import io.swagger.v3.oas.annotations.media.Schema;
@Schema(description = "Application log entry from OpenSearch")
public record LogEntryResponse(
@Schema(description = "Log timestamp (ISO-8601)") String timestamp,
@Schema(description = "Log level (INFO, WARN, ERROR, DEBUG)") String level,
@Schema(description = "Logger name") String loggerName,
@Schema(description = "Log message") String message,
@Schema(description = "Thread name") String threadName,
@Schema(description = "Stack trace (if present)") String stackTrace
) {}

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 p99DurationMs,
@NotNull double errorRate, @NotNull double errorRate,
@NotNull double throughputPerSec, @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( public record RouteSummary(
@NotNull String routeId, @NotNull String routeId,
@NotNull long exchangeCount, @NotNull long exchangeCount,
Instant lastSeen Instant lastSeen,
@Schema(description = "The from() endpoint URI, e.g. 'direct:processOrder'")
String fromEndpointUri
) {} ) {}

View File

@@ -0,0 +1,11 @@
package com.cameleer3.server.app.dto;
/**
* Request body for testing a tap expression against sample data via a live agent.
*
* @param expression the expression to evaluate (e.g. Simple, JSONPath, XPath)
* @param language the expression language identifier
* @param body sample message body to evaluate the expression against
* @param target what the expression targets (e.g. "body", "header", "property")
*/
public record TestExpressionRequest(String expression, String language, String body, String target) {}

View File

@@ -0,0 +1,9 @@
package com.cameleer3.server.app.dto;
/**
* Response from testing a tap expression against sample data.
*
* @param result the evaluation result (null if an error occurred)
* @param error error message if evaluation failed (null on success)
*/
public record TestExpressionResponse(String result, String error) {}

View File

@@ -0,0 +1,57 @@
package com.cameleer3.server.app.interceptor;
import com.cameleer3.server.core.admin.AuditCategory;
import com.cameleer3.server.core.admin.AuditResult;
import com.cameleer3.server.core.admin.AuditService;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import java.util.Map;
import java.util.Set;
/**
* Safety-net audit interceptor that logs a basic entry for any state-changing
* request (POST/PUT/DELETE) that was not explicitly audited by the controller.
* <p>
* Controllers that call {@link AuditService#log} set the {@code audit.logged}
* request attribute, which this interceptor checks to avoid double-recording.
*/
@Component
public class AuditInterceptor implements HandlerInterceptor {
private static final Set<String> AUDITABLE_METHODS = Set.of("POST", "PUT", "DELETE");
private static final Set<String> EXCLUDED_PATHS = Set.of("/api/v1/search/executions");
private final AuditService auditService;
public AuditInterceptor(AuditService auditService) {
this.auditService = auditService;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response,
Object handler, Exception ex) {
if (!AUDITABLE_METHODS.contains(request.getMethod())) {
return;
}
if (Boolean.TRUE.equals(request.getAttribute("audit.logged"))) {
return;
}
String path = request.getRequestURI();
if (EXCLUDED_PATHS.contains(path)) {
return;
}
AuditResult result = response.getStatus() < 400 ? AuditResult.SUCCESS : AuditResult.FAILURE;
auditService.log(
"HTTP " + request.getMethod() + " " + path,
AuditCategory.INFRA,
path,
Map.of("status", response.getStatus()),
result,
request);
}
}

View File

@@ -6,6 +6,8 @@ import com.cameleer3.server.core.search.SearchResult;
import com.cameleer3.server.core.storage.SearchIndex; import com.cameleer3.server.core.storage.SearchIndex;
import com.cameleer3.server.core.storage.model.ExecutionDocument; import com.cameleer3.server.core.storage.model.ExecutionDocument;
import com.cameleer3.server.core.storage.model.ExecutionDocument.ProcessorDoc; import com.cameleer3.server.core.storage.model.ExecutionDocument.ProcessorDoc;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.annotation.PostConstruct; import jakarta.annotation.PostConstruct;
import org.opensearch.client.json.JsonData; import org.opensearch.client.json.JsonData;
import org.opensearch.client.opensearch.OpenSearchClient; import org.opensearch.client.opensearch.OpenSearchClient;
@@ -33,6 +35,8 @@ public class OpenSearchIndex implements SearchIndex {
private static final Logger log = LoggerFactory.getLogger(OpenSearchIndex.class); private static final Logger log = LoggerFactory.getLogger(OpenSearchIndex.class);
private static final DateTimeFormatter DAY_FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd") private static final DateTimeFormatter DAY_FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd")
.withZone(ZoneOffset.UTC); .withZone(ZoneOffset.UTC);
private static final ObjectMapper JSON = new ObjectMapper();
private static final TypeReference<Map<String, String>> STR_MAP = new TypeReference<>() {};
private final OpenSearchClient client; private final OpenSearchClient client;
private final String indexPrefix; private final String indexPrefix;
@@ -125,6 +129,12 @@ public class OpenSearchIndex implements SearchIndex {
} }
} }
private static final List<String> HIGHLIGHT_FIELDS = List.of(
"error_message", "attributes_text",
"processors.input_body", "processors.output_body",
"processors.input_headers", "processors.output_headers",
"processors.attributes_text");
private org.opensearch.client.opensearch.core.SearchRequest buildSearchRequest( private org.opensearch.client.opensearch.core.SearchRequest buildSearchRequest(
SearchRequest request, int size) { SearchRequest request, int size) {
return org.opensearch.client.opensearch.core.SearchRequest.of(b -> { return org.opensearch.client.opensearch.core.SearchRequest.of(b -> {
@@ -137,6 +147,17 @@ public class OpenSearchIndex implements SearchIndex {
.field(request.sortColumn()) .field(request.sortColumn())
.order("asc".equalsIgnoreCase(request.sortDir()) .order("asc".equalsIgnoreCase(request.sortDir())
? SortOrder.Asc : SortOrder.Desc))); ? SortOrder.Asc : SortOrder.Desc)));
// Add highlight when full-text search is active
if (request.text() != null && !request.text().isBlank()) {
b.highlight(h -> {
for (String field : HIGHLIGHT_FIELDS) {
h.fields(field, hf -> hf
.fragmentSize(120)
.numberOfFragments(1));
}
return h;
});
}
return b; return b;
}); });
} }
@@ -158,14 +179,28 @@ public class OpenSearchIndex implements SearchIndex {
} }
// Keyword filters (use .keyword sub-field for exact matching on dynamically mapped text fields) // Keyword filters (use .keyword sub-field for exact matching on dynamically mapped text fields)
if (request.status() != null) if (request.status() != null && !request.status().isBlank()) {
filter.add(termQuery("status.keyword", request.status())); 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) if (request.routeId() != null)
filter.add(termQuery("route_id.keyword", request.routeId())); filter.add(termQuery("route_id.keyword", request.routeId()));
if (request.agentId() != null) if (request.agentId() != null)
filter.add(termQuery("agent_id.keyword", request.agentId())); filter.add(termQuery("agent_id.keyword", request.agentId()));
if (request.correlationId() != null) if (request.correlationId() != null)
filter.add(termQuery("correlation_id.keyword", request.correlationId())); filter.add(termQuery("correlation_id.keyword", request.correlationId()));
if (request.application() != null && !request.application().isBlank())
filter.add(termQuery("application_name.keyword", request.application()));
// Full-text search across all fields + nested processor fields // Full-text search across all fields + nested processor fields
if (request.text() != null && !request.text().isBlank()) { if (request.text() != null && !request.text().isBlank()) {
@@ -176,11 +211,13 @@ public class OpenSearchIndex implements SearchIndex {
// Search top-level text fields (analyzed match + wildcard for substring) // Search top-level text fields (analyzed match + wildcard for substring)
textQueries.add(Query.of(q -> q.multiMatch(m -> m textQueries.add(Query.of(q -> q.multiMatch(m -> m
.query(text) .query(text)
.fields("error_message", "error_stacktrace")))); .fields("error_message", "error_stacktrace", "attributes_text"))));
textQueries.add(Query.of(q -> q.wildcard(w -> w textQueries.add(Query.of(q -> q.wildcard(w -> w
.field("error_message").value(wildcard).caseInsensitive(true)))); .field("error_message").value(wildcard).caseInsensitive(true))));
textQueries.add(Query.of(q -> q.wildcard(w -> w textQueries.add(Query.of(q -> q.wildcard(w -> w
.field("error_stacktrace").value(wildcard).caseInsensitive(true)))); .field("error_stacktrace").value(wildcard).caseInsensitive(true))));
textQueries.add(Query.of(q -> q.wildcard(w -> w
.field("attributes_text").value(wildcard).caseInsensitive(true))));
// Search nested processor fields (analyzed match + wildcard) // Search nested processor fields (analyzed match + wildcard)
textQueries.add(Query.of(q -> q.nested(n -> n textQueries.add(Query.of(q -> q.nested(n -> n
@@ -189,14 +226,16 @@ public class OpenSearchIndex implements SearchIndex {
.query(text) .query(text)
.fields("processors.input_body", "processors.output_body", .fields("processors.input_body", "processors.output_body",
"processors.input_headers", "processors.output_headers", "processors.input_headers", "processors.output_headers",
"processors.error_message", "processors.error_stacktrace")))))); "processors.error_message", "processors.error_stacktrace",
"processors.attributes_text"))))));
textQueries.add(Query.of(q -> q.nested(n -> n textQueries.add(Query.of(q -> q.nested(n -> n
.path("processors") .path("processors")
.query(nq -> nq.bool(nb -> nb.should( .query(nq -> nq.bool(nb -> nb.should(
wildcardQuery("processors.input_body", wildcard), wildcardQuery("processors.input_body", wildcard),
wildcardQuery("processors.output_body", wildcard), wildcardQuery("processors.output_body", wildcard),
wildcardQuery("processors.input_headers", wildcard), wildcardQuery("processors.input_headers", wildcard),
wildcardQuery("processors.output_headers", wildcard) wildcardQuery("processors.output_headers", wildcard),
wildcardQuery("processors.attributes_text", wildcard)
).minimumShouldMatch("1")))))); ).minimumShouldMatch("1"))))));
// Also try keyword fields for exact matches // Also try keyword fields for exact matches
@@ -297,6 +336,11 @@ public class OpenSearchIndex implements SearchIndex {
map.put("duration_ms", doc.durationMs()); map.put("duration_ms", doc.durationMs());
map.put("error_message", doc.errorMessage()); map.put("error_message", doc.errorMessage());
map.put("error_stacktrace", doc.errorStacktrace()); map.put("error_stacktrace", doc.errorStacktrace());
if (doc.attributes() != null) {
Map<String, String> attrs = parseAttributesJson(doc.attributes());
map.put("attributes", attrs);
map.put("attributes_text", flattenAttributes(attrs));
}
if (doc.processors() != null) { if (doc.processors() != null) {
map.put("processors", doc.processors().stream().map(p -> { map.put("processors", doc.processors().stream().map(p -> {
Map<String, Object> pm = new LinkedHashMap<>(); Map<String, Object> pm = new LinkedHashMap<>();
@@ -309,9 +353,16 @@ public class OpenSearchIndex implements SearchIndex {
pm.put("output_body", p.outputBody()); pm.put("output_body", p.outputBody());
pm.put("input_headers", p.inputHeaders()); pm.put("input_headers", p.inputHeaders());
pm.put("output_headers", p.outputHeaders()); pm.put("output_headers", p.outputHeaders());
if (p.attributes() != null) {
Map<String, String> pAttrs = parseAttributesJson(p.attributes());
pm.put("attributes", pAttrs);
pm.put("attributes_text", flattenAttributes(pAttrs));
}
return pm; return pm;
}).toList()); }).toList());
} }
map.put("has_trace_data", doc.hasTraceData());
map.put("is_replay", doc.isReplay());
return map; return map;
} }
@@ -319,6 +370,22 @@ public class OpenSearchIndex implements SearchIndex {
private ExecutionSummary hitToSummary(Hit<Map> hit) { private ExecutionSummary hitToSummary(Hit<Map> hit) {
Map<String, Object> src = hit.source(); Map<String, Object> src = hit.source();
if (src == null) return null; if (src == null) return null;
@SuppressWarnings("unchecked")
Map<String, String> attributes = src.get("attributes") instanceof Map
? new LinkedHashMap<>((Map<String, String>) src.get("attributes")) : null;
// Merge processor-level attributes (execution-level takes precedence)
if (src.get("processors") instanceof List<?> procs) {
for (Object pObj : procs) {
if (pObj instanceof Map<?, ?> pm && pm.get("attributes") instanceof Map<?, ?> pa) {
if (attributes == null) attributes = new LinkedHashMap<>();
for (var entry : pa.entrySet()) {
attributes.putIfAbsent(
String.valueOf(entry.getKey()),
String.valueOf(entry.getValue()));
}
}
}
}
return new ExecutionSummary( return new ExecutionSummary(
(String) src.get("execution_id"), (String) src.get("execution_id"),
(String) src.get("route_id"), (String) src.get("route_id"),
@@ -330,7 +397,37 @@ public class OpenSearchIndex implements SearchIndex {
src.get("duration_ms") != null ? ((Number) src.get("duration_ms")).longValue() : 0L, src.get("duration_ms") != null ? ((Number) src.get("duration_ms")).longValue() : 0L,
(String) src.get("correlation_id"), (String) src.get("correlation_id"),
(String) src.get("error_message"), (String) src.get("error_message"),
null // diagramContentHash not stored in index null, // diagramContentHash not stored in index
extractHighlight(hit),
attributes,
Boolean.TRUE.equals(src.get("has_trace_data")),
Boolean.TRUE.equals(src.get("is_replay"))
); );
} }
private String extractHighlight(Hit<Map> hit) {
if (hit.highlight() == null || hit.highlight().isEmpty()) return null;
for (List<String> fragments : hit.highlight().values()) {
if (fragments != null && !fragments.isEmpty()) {
return fragments.get(0);
}
}
return null;
}
private static Map<String, String> parseAttributesJson(String json) {
if (json == null || json.isBlank()) return null;
try {
return JSON.readValue(json, STR_MAP);
} catch (Exception e) {
return null;
}
}
private static String flattenAttributes(Map<String, String> attrs) {
if (attrs == null || attrs.isEmpty()) return "";
return attrs.entrySet().stream()
.map(e -> e.getKey() + "=" + e.getValue())
.collect(Collectors.joining(" "));
}
} }

View File

@@ -0,0 +1,223 @@
package com.cameleer3.server.app.search;
import com.cameleer3.common.model.LogEntry;
import com.cameleer3.server.app.dto.LogEntryResponse;
import jakarta.annotation.PostConstruct;
import org.opensearch.client.json.JsonData;
import org.opensearch.client.opensearch.OpenSearchClient;
import org.opensearch.client.opensearch._types.FieldValue;
import org.opensearch.client.opensearch._types.SortOrder;
import org.opensearch.client.opensearch._types.mapping.Property;
import org.opensearch.client.opensearch._types.query_dsl.BoolQuery;
import org.opensearch.client.opensearch._types.query_dsl.Query;
import org.opensearch.client.opensearch.core.BulkRequest;
import org.opensearch.client.opensearch.core.BulkResponse;
import org.opensearch.client.opensearch.core.bulk.BulkResponseItem;
import org.opensearch.client.opensearch.indices.ExistsIndexTemplateRequest;
import org.opensearch.client.opensearch.indices.PutIndexTemplateRequest;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Repository;
import java.io.IOException;
import java.time.Instant;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
@Repository
public class OpenSearchLogIndex {
private static final Logger log = LoggerFactory.getLogger(OpenSearchLogIndex.class);
private static final DateTimeFormatter DAY_FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd")
.withZone(ZoneOffset.UTC);
private final OpenSearchClient client;
private final String indexPrefix;
private final int retentionDays;
public OpenSearchLogIndex(OpenSearchClient client,
@Value("${opensearch.log-index-prefix:logs-}") String indexPrefix,
@Value("${opensearch.log-retention-days:7}") int retentionDays) {
this.client = client;
this.indexPrefix = indexPrefix;
this.retentionDays = retentionDays;
}
@PostConstruct
void init() {
ensureIndexTemplate();
ensureIsmPolicy();
}
private void ensureIndexTemplate() {
String templateName = indexPrefix.replace("-", "") + "-template";
String indexPattern = indexPrefix + "*";
try {
boolean exists = client.indices().existsIndexTemplate(
ExistsIndexTemplateRequest.of(b -> b.name(templateName))).value();
if (!exists) {
client.indices().putIndexTemplate(PutIndexTemplateRequest.of(b -> b
.name(templateName)
.indexPatterns(List.of(indexPattern))
.template(t -> t
.settings(s -> s
.numberOfShards("1")
.numberOfReplicas("1"))
.mappings(m -> m
.properties("@timestamp", Property.of(p -> p.date(d -> d)))
.properties("level", Property.of(p -> p.keyword(k -> k)))
.properties("loggerName", Property.of(p -> p.keyword(k -> k)))
.properties("message", Property.of(p -> p.text(tx -> tx)))
.properties("threadName", Property.of(p -> p.keyword(k -> k)))
.properties("stackTrace", Property.of(p -> p.text(tx -> tx)))
.properties("agentId", Property.of(p -> p.keyword(k -> k)))
.properties("application", Property.of(p -> p.keyword(k -> k)))
.properties("exchangeId", Property.of(p -> p.keyword(k -> k)))))));
log.info("OpenSearch log index template '{}' created", templateName);
}
} catch (IOException e) {
log.error("Failed to create log index template", e);
}
}
private void ensureIsmPolicy() {
String policyId = "logs-retention";
try {
// Use the low-level REST client to manage ISM policies
var restClient = client._transport();
// Check if the ISM policy exists via a GET; create if not
// ISM is managed via the _plugins/_ism/policies API
// For now, log a reminder — ISM policy should be created via OpenSearch API or dashboard
log.info("Log retention policy: indices matching '{}*' should be deleted after {} days. " +
"Ensure ISM policy '{}' is configured in OpenSearch.", indexPrefix, retentionDays, policyId);
} catch (Exception e) {
log.warn("Could not verify ISM policy for log retention", e);
}
}
public List<LogEntryResponse> search(String application, String agentId, String level,
String query, String exchangeId,
Instant from, Instant to, int limit) {
try {
BoolQuery.Builder bool = new BoolQuery.Builder();
bool.must(Query.of(q -> q.term(t -> t.field("application").value(FieldValue.of(application)))));
if (agentId != null && !agentId.isEmpty()) {
bool.must(Query.of(q -> q.term(t -> t.field("agentId").value(FieldValue.of(agentId)))));
}
if (exchangeId != null && !exchangeId.isEmpty()) {
// Match on top-level field (new records) or MDC nested field (old records)
bool.must(Query.of(q -> q.bool(b -> b
.should(Query.of(s -> s.term(t -> t.field("exchangeId.keyword").value(FieldValue.of(exchangeId)))))
.should(Query.of(s -> s.term(t -> t.field("mdc.camel.exchangeId.keyword").value(FieldValue.of(exchangeId)))))
.minimumShouldMatch("1"))));
}
if (level != null && !level.isEmpty()) {
bool.must(Query.of(q -> q.term(t -> t.field("level").value(FieldValue.of(level.toUpperCase())))));
}
if (query != null && !query.isEmpty()) {
bool.must(Query.of(q -> q.match(m -> m.field("message").query(FieldValue.of(query)))));
}
if (from != null || to != null) {
bool.must(Query.of(q -> q.range(r -> {
r.field("@timestamp");
if (from != null) r.gte(JsonData.of(from.toString()));
if (to != null) r.lte(JsonData.of(to.toString()));
return r;
})));
}
var response = client.search(s -> s
.index(indexPrefix + "*")
.query(Query.of(q -> q.bool(bool.build())))
.sort(so -> so.field(f -> f.field("@timestamp").order(SortOrder.Desc)))
.size(limit), Map.class);
List<LogEntryResponse> results = new ArrayList<>();
for (var hit : response.hits().hits()) {
@SuppressWarnings("unchecked")
Map<String, Object> src = (Map<String, Object>) hit.source();
if (src == null) continue;
results.add(new LogEntryResponse(
str(src, "@timestamp"),
str(src, "level"),
str(src, "loggerName"),
str(src, "message"),
str(src, "threadName"),
str(src, "stackTrace")));
}
return results;
} catch (IOException e) {
log.error("Failed to search log entries for application={}", application, e);
return List.of();
}
}
private static String str(Map<String, Object> map, String key) {
Object v = map.get(key);
return v != null ? v.toString() : null;
}
public void indexBatch(String agentId, String application, List<LogEntry> entries) {
if (entries == null || entries.isEmpty()) {
return;
}
try {
BulkRequest.Builder bulkBuilder = new BulkRequest.Builder();
for (LogEntry entry : entries) {
String indexName = indexPrefix + DAY_FMT.format(
entry.getTimestamp() != null ? entry.getTimestamp() : java.time.Instant.now());
Map<String, Object> doc = toMap(entry, agentId, application);
bulkBuilder.operations(op -> op
.index(idx -> idx
.index(indexName)
.document(doc)));
}
BulkResponse response = client.bulk(bulkBuilder.build());
if (response.errors()) {
int errorCount = 0;
for (BulkResponseItem item : response.items()) {
if (item.error() != null) {
errorCount++;
if (errorCount == 1) {
log.error("Bulk log index error: {}", item.error().reason());
}
}
}
log.error("Bulk log indexing had {} error(s) out of {} entries", errorCount, entries.size());
} else {
log.debug("Indexed {} log entries for agent={}, app={}", entries.size(), agentId, application);
}
} catch (IOException e) {
log.error("Failed to bulk index {} log entries for agent={}", entries.size(), agentId, e);
}
}
private Map<String, Object> toMap(LogEntry entry, String agentId, String application) {
Map<String, Object> doc = new LinkedHashMap<>();
doc.put("@timestamp", entry.getTimestamp() != null ? entry.getTimestamp().toString() : null);
doc.put("level", entry.getLevel());
doc.put("loggerName", entry.getLoggerName());
doc.put("message", entry.getMessage());
doc.put("threadName", entry.getThreadName());
doc.put("stackTrace", entry.getStackTrace());
doc.put("mdc", entry.getMdc());
doc.put("agentId", agentId);
doc.put("application", application);
if (entry.getMdc() != null) {
String exId = entry.getMdc().get("camel.exchangeId");
if (exId != null) doc.put("exchangeId", exId);
}
return doc;
}
}

View File

@@ -159,6 +159,9 @@ public class OidcAuthController {
throw e; throw e;
} catch (Exception e) { } catch (Exception e) {
log.error("OIDC callback failed: {}", e.getMessage(), e); log.error("OIDC callback failed: {}", e.getMessage(), e);
auditService.log("unknown", "login_oidc", AuditCategory.AUTH, null,
Map.of("reason", e.getMessage() != null ? e.getMessage() : "unknown"),
AuditResult.FAILURE, httpRequest);
throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, throw new ResponseStatusException(HttpStatus.UNAUTHORIZED,
"OIDC authentication failed: " + e.getMessage()); "OIDC authentication failed: " + e.getMessage());
} }

View File

@@ -72,11 +72,16 @@ public class SecurityConfig {
.requestMatchers(HttpMethod.POST, "/api/v1/agents/*/commands").hasAnyRole("OPERATOR", "ADMIN") .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/groups/*/commands").hasAnyRole("OPERATOR", "ADMIN")
.requestMatchers(HttpMethod.POST, "/api/v1/agents/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 // Search endpoints
.requestMatchers(HttpMethod.GET, "/api/v1/search/**").hasAnyRole("VIEWER", "OPERATOR", "ADMIN", "AGENT") .requestMatchers(HttpMethod.GET, "/api/v1/search/**").hasAnyRole("VIEWER", "OPERATOR", "ADMIN", "AGENT")
.requestMatchers(HttpMethod.POST, "/api/v1/search/**").hasAnyRole("VIEWER", "OPERATOR", "ADMIN") .requestMatchers(HttpMethod.POST, "/api/v1/search/**").hasAnyRole("VIEWER", "OPERATOR", "ADMIN")
// Application config endpoints
.requestMatchers(HttpMethod.GET, "/api/v1/config/*").hasAnyRole("VIEWER", "OPERATOR", "ADMIN", "AGENT")
.requestMatchers(HttpMethod.PUT, "/api/v1/config/*").hasAnyRole("OPERATOR", "ADMIN")
// Read-only data endpoints — viewer+ // Read-only data endpoints — viewer+
.requestMatchers(HttpMethod.GET, "/api/v1/executions/**").hasAnyRole("VIEWER", "OPERATOR", "ADMIN") .requestMatchers(HttpMethod.GET, "/api/v1/executions/**").hasAnyRole("VIEWER", "OPERATOR", "ADMIN")
.requestMatchers(HttpMethod.GET, "/api/v1/diagrams/**").hasAnyRole("VIEWER", "OPERATOR", "ADMIN") .requestMatchers(HttpMethod.GET, "/api/v1/diagrams/**").hasAnyRole("VIEWER", "OPERATOR", "ADMIN")

View File

@@ -123,7 +123,8 @@ public class UiAuthController {
@ApiResponse(responseCode = "200", description = "Token refreshed") @ApiResponse(responseCode = "200", description = "Token refreshed")
@ApiResponse(responseCode = "401", description = "Invalid refresh token", @ApiResponse(responseCode = "401", description = "Invalid refresh token",
content = @Content(schema = @Schema(implementation = ErrorResponse.class))) content = @Content(schema = @Schema(implementation = ErrorResponse.class)))
public ResponseEntity<AuthTokenResponse> refresh(@RequestBody RefreshRequest request) { public ResponseEntity<AuthTokenResponse> refresh(@RequestBody RefreshRequest request,
HttpServletRequest httpRequest) {
try { try {
JwtValidationResult result = jwtService.validateRefreshToken(request.refreshToken()); JwtValidationResult result = jwtService.validateRefreshToken(request.refreshToken());
if (!result.subject().startsWith("user:")) { if (!result.subject().startsWith("user:")) {
@@ -138,6 +139,7 @@ public class UiAuthController {
String displayName = userRepository.findById(result.subject()) String displayName = userRepository.findById(result.subject())
.map(UserInfo::displayName) .map(UserInfo::displayName)
.orElse(result.subject()); .orElse(result.subject());
auditService.log(result.subject(), "token_refresh", AuditCategory.AUTH, null, null, AuditResult.SUCCESS, httpRequest);
return ResponseEntity.ok(new AuthTokenResponse(accessToken, refreshToken, displayName, null)); return ResponseEntity.ok(new AuthTokenResponse(accessToken, refreshToken, displayName, null));
} catch (ResponseStatusException e) { } catch (ResponseStatusException e) {
throw e; throw e;

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

@@ -0,0 +1,77 @@
package com.cameleer3.server.app.storage;
import com.cameleer3.common.model.ApplicationConfig;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;
import java.util.List;
import java.util.Optional;
@Repository
public class PostgresApplicationConfigRepository {
private final JdbcTemplate jdbc;
private final ObjectMapper objectMapper;
public PostgresApplicationConfigRepository(JdbcTemplate jdbc, ObjectMapper objectMapper) {
this.jdbc = jdbc;
this.objectMapper = objectMapper;
}
public List<ApplicationConfig> findAll() {
return jdbc.query(
"SELECT config_val, version, updated_at FROM application_config ORDER BY application",
(rs, rowNum) -> {
try {
ApplicationConfig cfg = objectMapper.readValue(rs.getString("config_val"), ApplicationConfig.class);
cfg.setVersion(rs.getInt("version"));
cfg.setUpdatedAt(rs.getTimestamp("updated_at").toInstant());
return cfg;
} catch (JsonProcessingException e) {
throw new RuntimeException("Failed to deserialize application config", e);
}
});
}
public Optional<ApplicationConfig> findByApplication(String application) {
List<ApplicationConfig> results = jdbc.query(
"SELECT config_val, version, updated_at FROM application_config WHERE application = ?",
(rs, rowNum) -> {
try {
ApplicationConfig cfg = objectMapper.readValue(rs.getString("config_val"), ApplicationConfig.class);
cfg.setVersion(rs.getInt("version"));
cfg.setUpdatedAt(rs.getTimestamp("updated_at").toInstant());
return cfg;
} catch (JsonProcessingException e) {
throw new RuntimeException("Failed to deserialize application config", e);
}
},
application);
return results.isEmpty() ? Optional.empty() : Optional.of(results.get(0));
}
public ApplicationConfig save(String application, ApplicationConfig config, String updatedBy) {
String json;
try {
json = objectMapper.writeValueAsString(config);
} catch (JsonProcessingException e) {
throw new RuntimeException("Failed to serialize application config", e);
}
// Upsert: insert or update, auto-increment version
int updated = jdbc.update("""
INSERT INTO application_config (application, config_val, version, updated_at, updated_by)
VALUES (?, ?::jsonb, 1, now(), ?)
ON CONFLICT (application) DO UPDATE SET
config_val = EXCLUDED.config_val,
version = application_config.version + 1,
updated_at = now(),
updated_by = EXCLUDED.updated_by
""",
application, json, updatedBy);
return findByApplication(application).orElseThrow();
}
}

View File

@@ -16,6 +16,7 @@ import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException; import java.security.NoSuchAlgorithmException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.HashMap;
import java.util.HexFormat; import java.util.HexFormat;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@@ -33,8 +34,8 @@ public class PostgresDiagramStore implements DiagramStore {
private static final Logger log = LoggerFactory.getLogger(PostgresDiagramStore.class); private static final Logger log = LoggerFactory.getLogger(PostgresDiagramStore.class);
private static final String INSERT_SQL = """ private static final String INSERT_SQL = """
INSERT INTO route_diagrams (content_hash, route_id, agent_id, definition) INSERT INTO route_diagrams (content_hash, route_id, agent_id, application_name, definition)
VALUES (?, ?, ?, ?::jsonb) VALUES (?, ?, ?, ?, ?::jsonb)
ON CONFLICT (content_hash) DO NOTHING ON CONFLICT (content_hash) DO NOTHING
"""; """;
@@ -62,11 +63,12 @@ public class PostgresDiagramStore implements DiagramStore {
try { try {
RouteGraph graph = diagram.graph(); RouteGraph graph = diagram.graph();
String agentId = diagram.agentId() != null ? diagram.agentId() : ""; String agentId = diagram.agentId() != null ? diagram.agentId() : "";
String applicationName = diagram.applicationName() != null ? diagram.applicationName() : "";
String json = objectMapper.writeValueAsString(graph); String json = objectMapper.writeValueAsString(graph);
String contentHash = sha256Hex(json); String contentHash = sha256Hex(json);
String routeId = graph.getRouteId() != null ? graph.getRouteId() : ""; String routeId = graph.getRouteId() != null ? graph.getRouteId() : "";
jdbcTemplate.update(INSERT_SQL, contentHash, routeId, agentId, json); jdbcTemplate.update(INSERT_SQL, contentHash, routeId, agentId, applicationName, json);
log.debug("Stored diagram for route={} agent={} with hash={}", routeId, agentId, contentHash); log.debug("Stored diagram for route={} agent={} with hash={}", routeId, agentId, contentHash);
} catch (JsonProcessingException e) { } catch (JsonProcessingException e) {
throw new RuntimeException("Failed to serialize RouteGraph to JSON", e); throw new RuntimeException("Failed to serialize RouteGraph to JSON", e);
@@ -116,6 +118,21 @@ public class PostgresDiagramStore implements DiagramStore {
return Optional.of((String) rows.get(0).get("content_hash")); return Optional.of((String) rows.get(0).get("content_hash"));
} }
@Override
public Map<String, String> findProcessorRouteMapping(String applicationName) {
Map<String, String> mapping = new HashMap<>();
jdbcTemplate.query("""
SELECT DISTINCT rd.route_id, node_elem->>'id' AS processor_id
FROM route_diagrams rd,
jsonb_array_elements(rd.definition::jsonb->'nodes') AS node_elem
WHERE rd.application_name = ?
AND node_elem->>'id' IS NOT NULL
""",
rs -> { mapping.put(rs.getString("processor_id"), rs.getString("route_id")); },
applicationName);
return mapping;
}
static String sha256Hex(String input) { static String sha256Hex(String input) {
try { try {
MessageDigest digest = MessageDigest.getInstance("SHA-256"); MessageDigest digest = MessageDigest.getInstance("SHA-256");

View File

@@ -27,8 +27,14 @@ public class PostgresExecutionStore implements ExecutionStore {
INSERT INTO executions (execution_id, route_id, agent_id, application_name, INSERT INTO executions (execution_id, route_id, agent_id, application_name,
status, correlation_id, exchange_id, start_time, end_time, status, correlation_id, exchange_id, start_time, end_time,
duration_ms, error_message, error_stacktrace, diagram_content_hash, duration_ms, error_message, error_stacktrace, diagram_content_hash,
engine_level, input_body, output_body, input_headers, output_headers,
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) created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, now(), now()) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?::jsonb, ?::jsonb, ?::jsonb,
?, ?, ?, ?, ?, ?, ?::jsonb, ?, ?, now(), now())
ON CONFLICT (execution_id, start_time) DO UPDATE SET ON CONFLICT (execution_id, start_time) DO UPDATE SET
status = CASE status = CASE
WHEN EXCLUDED.status IN ('COMPLETED', 'FAILED') WHEN EXCLUDED.status IN ('COMPLETED', 'FAILED')
@@ -42,6 +48,21 @@ public class PostgresExecutionStore implements ExecutionStore {
error_message = COALESCE(EXCLUDED.error_message, executions.error_message), error_message = COALESCE(EXCLUDED.error_message, executions.error_message),
error_stacktrace = COALESCE(EXCLUDED.error_stacktrace, executions.error_stacktrace), error_stacktrace = COALESCE(EXCLUDED.error_stacktrace, executions.error_stacktrace),
diagram_content_hash = COALESCE(EXCLUDED.diagram_content_hash, executions.diagram_content_hash), diagram_content_hash = COALESCE(EXCLUDED.diagram_content_hash, executions.diagram_content_hash),
engine_level = COALESCE(EXCLUDED.engine_level, executions.engine_level),
input_body = COALESCE(EXCLUDED.input_body, executions.input_body),
output_body = COALESCE(EXCLUDED.output_body, executions.output_body),
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() updated_at = now()
""", """,
execution.executionId(), execution.routeId(), execution.agentId(), execution.executionId(), execution.routeId(), execution.agentId(),
@@ -50,7 +71,15 @@ public class PostgresExecutionStore implements ExecutionStore {
Timestamp.from(execution.startTime()), Timestamp.from(execution.startTime()),
execution.endTime() != null ? Timestamp.from(execution.endTime()) : null, execution.endTime() != null ? Timestamp.from(execution.endTime()) : null,
execution.durationMs(), execution.errorMessage(), execution.durationMs(), execution.errorMessage(),
execution.errorStacktrace(), execution.diagramContentHash()); execution.errorStacktrace(), execution.diagramContentHash(),
execution.engineLevel(),
execution.inputBody(), execution.outputBody(),
execution.inputHeaders(), execution.outputHeaders(),
execution.attributes(),
execution.errorType(), execution.errorCategory(),
execution.rootCauseType(), execution.rootCauseMessage(),
execution.traceId(), execution.spanId(),
execution.processorsJson(), execution.hasTraceData(), execution.isReplay());
} }
@Override @Override
@@ -59,10 +88,15 @@ public class PostgresExecutionStore implements ExecutionStore {
List<ProcessorRecord> processors) { List<ProcessorRecord> processors) {
jdbc.batchUpdate(""" jdbc.batchUpdate("""
INSERT INTO processor_executions (execution_id, processor_id, processor_type, INSERT INTO processor_executions (execution_id, processor_id, processor_type,
diagram_node_id, application_name, route_id, depth, parent_processor_id, application_name, route_id, depth, parent_processor_id,
status, start_time, end_time, duration_ms, error_message, error_stacktrace, status, start_time, end_time, duration_ms, error_message, error_stacktrace,
input_body, output_body, input_headers, output_headers) input_body, output_body, input_headers, output_headers, attributes,
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?::jsonb, ?::jsonb) 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 ON CONFLICT (execution_id, processor_id, start_time) DO UPDATE SET
status = EXCLUDED.status, status = EXCLUDED.status,
end_time = COALESCE(EXCLUDED.end_time, processor_executions.end_time), end_time = COALESCE(EXCLUDED.end_time, processor_executions.end_time),
@@ -72,16 +106,38 @@ public class PostgresExecutionStore implements ExecutionStore {
input_body = COALESCE(EXCLUDED.input_body, processor_executions.input_body), input_body = COALESCE(EXCLUDED.input_body, processor_executions.input_body),
output_body = COALESCE(EXCLUDED.output_body, processor_executions.output_body), output_body = COALESCE(EXCLUDED.output_body, processor_executions.output_body),
input_headers = COALESCE(EXCLUDED.input_headers, processor_executions.input_headers), input_headers = COALESCE(EXCLUDED.input_headers, processor_executions.input_headers),
output_headers = COALESCE(EXCLUDED.output_headers, processor_executions.output_headers) output_headers = COALESCE(EXCLUDED.output_headers, processor_executions.output_headers),
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[]{ processors.stream().map(p -> new Object[]{
p.executionId(), p.processorId(), p.processorType(), p.executionId(), p.processorId(), p.processorType(),
p.diagramNodeId(), p.applicationName(), p.routeId(), p.applicationName(), p.routeId(),
p.depth(), p.parentProcessorId(), p.status(), p.depth(), p.parentProcessorId(), p.status(),
Timestamp.from(p.startTime()), Timestamp.from(p.startTime()),
p.endTime() != null ? Timestamp.from(p.endTime()) : null, p.endTime() != null ? Timestamp.from(p.endTime()) : null,
p.durationMs(), p.errorMessage(), p.errorStacktrace(), p.durationMs(), p.errorMessage(), p.errorStacktrace(),
p.inputBody(), p.outputBody(), p.inputHeaders(), p.outputHeaders() p.inputBody(), p.outputBody(), p.inputHeaders(), p.outputHeaders(),
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()); }).toList());
} }
@@ -100,6 +156,13 @@ public class PostgresExecutionStore implements ExecutionStore {
PROCESSOR_MAPPER, executionId); 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) -> private static final RowMapper<ExecutionRecord> EXECUTION_MAPPER = (rs, rowNum) ->
new ExecutionRecord( new ExecutionRecord(
rs.getString("execution_id"), rs.getString("route_id"), rs.getString("execution_id"), rs.getString("route_id"),
@@ -109,12 +172,22 @@ public class PostgresExecutionStore implements ExecutionStore {
toInstant(rs, "start_time"), toInstant(rs, "end_time"), toInstant(rs, "start_time"), toInstant(rs, "end_time"),
rs.getObject("duration_ms") != null ? rs.getLong("duration_ms") : null, rs.getObject("duration_ms") != null ? rs.getLong("duration_ms") : null,
rs.getString("error_message"), rs.getString("error_stacktrace"), rs.getString("error_message"), rs.getString("error_stacktrace"),
rs.getString("diagram_content_hash")); rs.getString("diagram_content_hash"),
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("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) -> private static final RowMapper<ProcessorRecord> PROCESSOR_MAPPER = (rs, rowNum) ->
new ProcessorRecord( new ProcessorRecord(
rs.getString("execution_id"), rs.getString("processor_id"), rs.getString("execution_id"), rs.getString("processor_id"),
rs.getString("processor_type"), rs.getString("diagram_node_id"), rs.getString("processor_type"),
rs.getString("application_name"), rs.getString("route_id"), rs.getString("application_name"), rs.getString("route_id"),
rs.getInt("depth"), rs.getString("parent_processor_id"), rs.getInt("depth"), rs.getString("parent_processor_id"),
rs.getString("status"), rs.getString("status"),
@@ -122,7 +195,18 @@ public class PostgresExecutionStore implements ExecutionStore {
rs.getObject("duration_ms") != null ? rs.getLong("duration_ms") : null, rs.getObject("duration_ms") != null ? rs.getLong("duration_ms") : null,
rs.getString("error_message"), rs.getString("error_stacktrace"), rs.getString("error_message"), rs.getString("error_stacktrace"),
rs.getString("input_body"), rs.getString("output_body"), rs.getString("input_body"), rs.getString("output_body"),
rs.getString("input_headers"), rs.getString("output_headers")); rs.getString("input_headers"), rs.getString("output_headers"),
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 { private static Instant toInstant(ResultSet rs, String column) throws SQLException {
Timestamp ts = rs.getTimestamp(column); 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.ExecutionStats;
import com.cameleer3.server.core.search.StatsTimeseries; import com.cameleer3.server.core.search.StatsTimeseries;
import com.cameleer3.server.core.search.StatsTimeseries.TimeseriesBucket; import com.cameleer3.server.core.search.StatsTimeseries.TimeseriesBucket;
import com.cameleer3.server.core.search.TopError;
import com.cameleer3.server.core.storage.StatsStore; import com.cameleer3.server.core.storage.StatsStore;
import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository; import org.springframework.stereotype.Repository;
@@ -12,7 +13,9 @@ import java.time.Duration;
import java.time.Instant; import java.time.Instant;
import java.time.temporal.ChronoUnit; import java.time.temporal.ChronoUnit;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.Map;
@Repository @Repository
public class PostgresStatsStore implements StatsStore { public class PostgresStatsStore implements StatsStore {
@@ -184,4 +187,242 @@ public class PostgresStatsStore implements StatsStore {
return new StatsTimeseries(buckets); 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 = { @GetMapping(value = {
"/login", "/login",
"/executions", "/executions",
"/executions/{path:[^\\.]*}", "/executions/**",
"/oidc/callback", "/oidc/callback",
"/admin/{path:[^\\.]*}" "/admin/**"
}) })
public String forward() { public String forward() {
return "forward:/index.html"; return "forward:/index.html";

View File

@@ -42,6 +42,8 @@ opensearch:
index-prefix: ${CAMELEER_OPENSEARCH_INDEX_PREFIX:executions-} index-prefix: ${CAMELEER_OPENSEARCH_INDEX_PREFIX:executions-}
queue-size: ${CAMELEER_OPENSEARCH_QUEUE_SIZE:10000} queue-size: ${CAMELEER_OPENSEARCH_QUEUE_SIZE:10000}
debounce-ms: ${CAMELEER_OPENSEARCH_DEBOUNCE_MS:2000} debounce-ms: ${CAMELEER_OPENSEARCH_DEBOUNCE_MS:2000}
log-index-prefix: ${CAMELEER_LOG_INDEX_PREFIX:logs-}
log-retention-days: ${CAMELEER_LOG_RETENTION_DAYS:7}
cameleer: cameleer:
body-size-limit: ${CAMELEER_BODY_SIZE_LIMIT:16384} body-size-limit: ${CAMELEER_BODY_SIZE_LIMIT:16384}

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,9 @@
-- Add engine level and route-level snapshot columns to executions table.
-- Required for REGULAR engine level where route-level payloads exist but
-- no processor execution records are created.
ALTER TABLE executions ADD COLUMN IF NOT EXISTS engine_level VARCHAR(16);
ALTER TABLE executions ADD COLUMN IF NOT EXISTS input_body TEXT;
ALTER TABLE executions ADD COLUMN IF NOT EXISTS output_body TEXT;
ALTER TABLE executions ADD COLUMN IF NOT EXISTS input_headers JSONB;
ALTER TABLE executions ADD COLUMN IF NOT EXISTS output_headers JSONB;

View File

@@ -0,0 +1,9 @@
-- Per-application configuration for agent observability settings.
-- Agents download this at startup and receive updates via SSE CONFIG_UPDATE.
CREATE TABLE application_config (
application TEXT PRIMARY KEY,
config_val JSONB NOT NULL,
version INTEGER NOT NULL DEFAULT 1,
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_by TEXT
);

View File

@@ -0,0 +1,2 @@
ALTER TABLE executions ADD COLUMN IF NOT EXISTS attributes JSONB;
ALTER TABLE processor_executions ADD COLUMN IF NOT EXISTS attributes JSONB;

View File

@@ -0,0 +1 @@
ALTER TABLE processor_executions DROP COLUMN IF EXISTS diagram_node_id;

View File

@@ -0,0 +1,2 @@
ALTER TABLE route_diagrams ADD COLUMN IF NOT EXISTS application_name TEXT NOT NULL DEFAULT '';
CREATE INDEX IF NOT EXISTS idx_diagrams_application ON route_diagrams (application_name);

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

@@ -50,11 +50,11 @@ class BackpressureIT extends AbstractPostgresIT {
// Fill the metrics buffer completely with a batch of 5 // Fill the metrics buffer completely with a batch of 5
String batchJson = """ String batchJson = """
[ [
{"agentId":"bp-agent","timestamp":"2026-03-11T10:00:00Z","metrics":{}}, {"agentId":"bp-agent","collectedAt":"2026-03-11T10:00:00Z","metricName":"test.metric","metricValue":1.0,"tags":{}},
{"agentId":"bp-agent","timestamp":"2026-03-11T10:00:01Z","metrics":{}}, {"agentId":"bp-agent","collectedAt":"2026-03-11T10:00:01Z","metricName":"test.metric","metricValue":2.0,"tags":{}},
{"agentId":"bp-agent","timestamp":"2026-03-11T10:00:02Z","metrics":{}}, {"agentId":"bp-agent","collectedAt":"2026-03-11T10:00:02Z","metricName":"test.metric","metricValue":3.0,"tags":{}},
{"agentId":"bp-agent","timestamp":"2026-03-11T10:00:03Z","metrics":{}}, {"agentId":"bp-agent","collectedAt":"2026-03-11T10:00:03Z","metricName":"test.metric","metricValue":4.0,"tags":{}},
{"agentId":"bp-agent","timestamp":"2026-03-11T10:00:04Z","metrics":{}} {"agentId":"bp-agent","collectedAt":"2026-03-11T10:00:04Z","metricName":"test.metric","metricValue":5.0,"tags":{}}
] ]
"""; """;
@@ -66,7 +66,7 @@ class BackpressureIT extends AbstractPostgresIT {
// Now buffer should be full -- next POST should get 503 // Now buffer should be full -- next POST should get 503
String overflowJson = """ String overflowJson = """
[{"agentId":"bp-agent","timestamp":"2026-03-11T10:00:05Z","metrics":{}}] [{"agentId":"bp-agent","collectedAt":"2026-03-11T10:00:05Z","metricName":"test.metric","metricValue":6.0,"tags":{}}]
"""; """;
ResponseEntity<String> response = restTemplate.postForEntity( ResponseEntity<String> response = restTemplate.postForEntity(

View File

@@ -65,7 +65,6 @@ class DetailControllerIT extends AbstractPostgresIT {
"startTime": "2026-03-10T10:00:00Z", "startTime": "2026-03-10T10:00:00Z",
"endTime": "2026-03-10T10:00:01Z", "endTime": "2026-03-10T10:00:01Z",
"durationMs": 1000, "durationMs": 1000,
"diagramNodeId": "node-root",
"inputBody": "root-input-body", "inputBody": "root-input-body",
"outputBody": "root-output-body", "outputBody": "root-output-body",
"inputHeaders": {"Content-Type": "application/json"}, "inputHeaders": {"Content-Type": "application/json"},
@@ -78,7 +77,6 @@ class DetailControllerIT extends AbstractPostgresIT {
"startTime": "2026-03-10T10:00:00.100Z", "startTime": "2026-03-10T10:00:00.100Z",
"endTime": "2026-03-10T10:00:00.200Z", "endTime": "2026-03-10T10:00:00.200Z",
"durationMs": 100, "durationMs": 100,
"diagramNodeId": "node-child1",
"inputBody": "child1-input", "inputBody": "child1-input",
"outputBody": "child1-output", "outputBody": "child1-output",
"inputHeaders": {}, "inputHeaders": {},
@@ -91,7 +89,6 @@ class DetailControllerIT extends AbstractPostgresIT {
"startTime": "2026-03-10T10:00:00.200Z", "startTime": "2026-03-10T10:00:00.200Z",
"endTime": "2026-03-10T10:00:00.800Z", "endTime": "2026-03-10T10:00:00.800Z",
"durationMs": 600, "durationMs": 600,
"diagramNodeId": "node-child2",
"inputBody": "child2-input", "inputBody": "child2-input",
"outputBody": "child2-output", "outputBody": "child2-output",
"inputHeaders": {}, "inputHeaders": {},
@@ -104,7 +101,6 @@ class DetailControllerIT extends AbstractPostgresIT {
"startTime": "2026-03-10T10:00:00.300Z", "startTime": "2026-03-10T10:00:00.300Z",
"endTime": "2026-03-10T10:00:00.700Z", "endTime": "2026-03-10T10:00:00.700Z",
"durationMs": 400, "durationMs": 400,
"diagramNodeId": "node-gc",
"inputBody": "gc-input", "inputBody": "gc-input",
"outputBody": "gc-output", "outputBody": "gc-output",
"inputHeaders": {"X-GC": "true"}, "inputHeaders": {"X-GC": "true"},

View File

@@ -39,8 +39,7 @@ class DiagramControllerIT extends AbstractPostgresIT {
"description": "Test route", "description": "Test route",
"version": 1, "version": 1,
"nodes": [], "nodes": [],
"edges": [], "edges": []
"processorNodeMapping": {}
} }
"""; """;
@@ -60,8 +59,7 @@ class DiagramControllerIT extends AbstractPostgresIT {
"description": "Flush test", "description": "Flush test",
"version": 1, "version": 1,
"nodes": [], "nodes": [],
"edges": [], "edges": []
"processorNodeMapping": {}
} }
"""; """;

View File

@@ -53,8 +53,7 @@ class DiagramRenderControllerIT extends AbstractPostgresIT {
"edges": [ "edges": [
{"source": "n1", "target": "n2", "edgeType": "FLOW"}, {"source": "n1", "target": "n2", "edgeType": "FLOW"},
{"source": "n2", "target": "n3", "edgeType": "FLOW"} {"source": "n2", "target": "n3", "edgeType": "FLOW"}
], ]
"processorNodeMapping": {}
} }
"""; """;

View File

@@ -39,7 +39,8 @@ class ElkDiagramRendererTest {
RouteNode process = new RouteNode("node-2", NodeType.BEAN, "myProcessor"); RouteNode process = new RouteNode("node-2", NodeType.BEAN, "myProcessor");
RouteNode to = new RouteNode("node-3", NodeType.TO, "log:output"); 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( graph.setEdges(List.of(
new RouteEdge("node-1", "node-2", RouteEdge.EdgeType.FLOW), new RouteEdge("node-1", "node-2", RouteEdge.EdgeType.FLOW),
new RouteEdge("node-2", "node-3", 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 otherwise = new RouteNode("node-4", NodeType.EIP_OTHERWISE, "otherwise");
RouteNode to = new RouteNode("node-5", NodeType.TO, "log:result"); 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)); choice.setChildren(List.of(when, otherwise));
from.setChildren(List.of(choice, to));
graph.setNodes(List.of(from, choice, when, otherwise, to)); graph.setRoot(from);
graph.setEdges(List.of( graph.setEdges(List.of(
new RouteEdge("node-1", "node-2", RouteEdge.EdgeType.FLOW), new RouteEdge("node-1", "node-2", RouteEdge.EdgeType.FLOW),
new RouteEdge("node-2", "node-3", 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)"); 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 @Test
void renderSvg_compoundGraph_producesValidSvg() { void renderSvg_compoundGraph_producesValidSvg() {
String svg = renderer.renderSvg(buildCompoundGraph()); String svg = renderer.renderSvg(buildCompoundGraph());

View File

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

View File

@@ -46,8 +46,7 @@ class DiagramLinkingIT extends AbstractPostgresIT {
], ],
"edges": [ "edges": [
{"source": "n1", "target": "n2", "edgeType": "FLOW"} {"source": "n1", "target": "n2", "edgeType": "FLOW"}
], ]
"processorNodeMapping": {}
} }
"""; """;

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