Files
cameleer-server/docs/superpowers/plans/2026-03-26-taps-attributes-replay.md
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

33 KiB

Taps, Business Attributes & Enhanced Replay — Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: Add UI and backend support for tap management, business attribute display, enhanced replay, per-route recording toggles, and success compression.

Architecture: Backend-first approach — add attributes to the execution pipeline, then build the command infrastructure for test-expression and replay, then layer on the frontend features page by page. Each task produces a self-contained, committable unit.

Tech Stack: Java 17 / Spring Boot 3.4 (backend), React 18 / TypeScript / TanStack Query (frontend), @cameleer/design-system components, PostgreSQL (JSONB), OpenSearch.

Spec: docs/superpowers/specs/2026-03-26-taps-attributes-replay-ui-design.md


File Map

Backend — New Files

  • cameleer3-server-app/src/main/resources/db/migration/V5__attributes.sql — Flyway migration adding attributes JSONB to executions and processor_executions tables
  • cameleer3-server-app/src/main/java/com/cameleer3/server/app/dto/TestExpressionRequest.java — Request DTO for test-expression endpoint
  • cameleer3-server-app/src/main/java/com/cameleer3/server/app/dto/TestExpressionResponse.java — Response DTO for test-expression endpoint

Backend — Modified Files

  • cameleer3-server-core/src/main/java/com/cameleer3/server/core/agent/CommandType.java — add TEST_EXPRESSION
  • cameleer3-server-core/src/main/java/com/cameleer3/server/core/storage/ExecutionStore.java — add attributes to ExecutionRecord and ProcessorRecord
  • cameleer3-server-core/src/main/java/com/cameleer3/server/core/detail/ExecutionDetail.java — add attributes field
  • cameleer3-server-core/src/main/java/com/cameleer3/server/core/detail/ProcessorNode.java — add attributes field
  • cameleer3-server-core/src/main/java/com/cameleer3/server/core/detail/DetailService.java — pass attributes through tree reconstruction
  • cameleer3-server-core/src/main/java/com/cameleer3/server/core/search/ExecutionSummary.java — add attributes field
  • cameleer3-server-core/src/main/java/com/cameleer3/server/core/ingestion/IngestionService.java — extract attributes from RouteExecution/ProcessorExecution
  • cameleer3-server-core/src/main/java/com/cameleer3/server/core/storage/model/ExecutionDocument.java — add attributes to ProcessorDoc
  • cameleer3-server-core/src/main/java/com/cameleer3/server/core/indexing/SearchIndexer.java — include attributes in indexing
  • cameleer3-server-core/src/main/java/com/cameleer3/server/core/agent/AgentRegistryService.java — add CompletableFuture-based command reply support
  • cameleer3-server-app/src/main/java/com/cameleer3/server/app/storage/PostgresExecutionStore.java — add attributes to INSERT/UPDATE queries
  • cameleer3-server-app/src/main/java/com/cameleer3/server/app/search/OpenSearchIndex.java — add attributes to toMap() and fromSearchHit()
  • cameleer3-server-app/src/main/java/com/cameleer3/server/app/controller/ApplicationConfigController.java — add test-expression endpoint
  • cameleer3-server-app/src/main/java/com/cameleer3/server/app/controller/AgentCommandController.java — add test-expression mapping, complete futures on ACK
  • cameleer3-server-app/src/main/java/com/cameleer3/server/app/dto/CommandAckRequest.java — add optional data field

Frontend — Modified Files

  • ui/src/api/schema.d.ts — add attributes to ExecutionDetail, ProcessorNode, ExecutionSummary
  • ui/src/api/queries/commands.ts — add TapDefinition type, extend ApplicationConfig, add test-expression mutation, add replay mutation
  • ui/src/pages/ExchangeDetail/ExchangeDetail.tsx — attributes strip, per-processor attributes, replay modal
  • ui/src/pages/ExchangeDetail/ExchangeDetail.module.css — attributes strip and replay styles
  • ui/src/pages/Dashboard/Dashboard.tsx — attributes column in exchanges table
  • ui/src/pages/Routes/RouteDetail.tsx — recording toggle, taps tab, tap modal with test
  • ui/src/pages/Routes/RouteDetail.module.css — taps and recording styles
  • ui/src/pages/Admin/AppConfigDetailPage.tsx — restructure to 3 sections
  • ui/src/pages/Admin/AppConfigDetailPage.module.css — updated styles

Task 1: Verify Prerequisites and Database Migration

Files:

  • Create: cameleer3-server-app/src/main/resources/db/migration/V5__attributes.sql

  • Step 1: Verify cameleer3-common has attributes support

Confirm the cameleer3-common SNAPSHOT dependency includes RouteExecution.getAttributes() and ProcessorExecution.getAttributes(). Run:

mvn dependency:sources -pl cameleer3-server-core -q

Then inspect the source jar for RouteExecution.java to confirm the attributes field exists. If it does not, the dependency must be updated first.

  • Step 2: Write migration SQL
-- V5__attributes.sql
ALTER TABLE executions ADD COLUMN IF NOT EXISTS attributes JSONB;
ALTER TABLE processor_executions ADD COLUMN IF NOT EXISTS attributes JSONB;
  • Step 3: Verify migration compiles

Run: cd cameleer3-server-app && mvn compile -pl . -q Expected: BUILD SUCCESS

  • Step 4: Commit
git add cameleer3-server-app/src/main/resources/db/migration/V5__attributes.sql
git commit -m "feat: add attributes JSONB columns to executions and processor_executions"

Task 2: Backend — Add Attributes to Storage Records and Detail Models

Files:

  • Modify: cameleer3-server-core/src/main/java/com/cameleer3/server/core/storage/ExecutionStore.java

  • Modify: cameleer3-server-core/src/main/java/com/cameleer3/server/core/detail/ExecutionDetail.java

  • Modify: cameleer3-server-core/src/main/java/com/cameleer3/server/core/detail/ProcessorNode.java

  • Modify: cameleer3-server-core/src/main/java/com/cameleer3/server/core/search/ExecutionSummary.java

  • Step 1: Add attributes field to ExecutionRecord

In ExecutionStore.java, add String attributes (JSONB as string) as the last parameter of the ExecutionRecord record. This is a serialized Map<String, String>.

  • Step 2: Add attributes field to ProcessorRecord

In ExecutionStore.java, add String attributes (JSONB as string) as the last parameter of the ProcessorRecord record.

  • Step 3: Add attributes field to ExecutionDetail

Add Map<String, String> attributes as the last parameter of the ExecutionDetail record (after outputHeaders).

  • Step 4: Add attributes field to ProcessorNode

ProcessorNode is a mutable class with a constructor. Add a Map<String, String> attributes field with getter. Add it to the constructor. Update the existing ProcessorNode constructor calls in DetailService.java to pass null or the attributes map.

  • Step 5: Add attributes field to ExecutionSummary

Add Map<String, String> attributes as the last parameter (after highlight).

  • Step 6: Verify compilation

Run: mvn compile -q Expected: Compilation errors in files that construct these records — these will be fixed in the next tasks.

  • Step 7: Commit
git add cameleer3-server-core/
git commit -m "feat: add attributes field to ExecutionRecord, ProcessorRecord, ExecutionDetail, ProcessorNode, ExecutionSummary"

Task 3: Backend — Attributes Ingestion Pipeline

Files:

  • Modify: cameleer3-server-core/src/main/java/com/cameleer3/server/core/ingestion/IngestionService.java

  • Modify: cameleer3-server-app/src/main/java/com/cameleer3/server/app/storage/PostgresExecutionStore.java

  • Step 1: Extract attributes in IngestionService.toExecutionRecord()

In the toExecutionRecord() method (~line 76-111), serialize execution.getAttributes() to JSON string using Jackson ObjectMapper. Pass it as the new attributes parameter to ExecutionRecord. If attributes is null or empty, pass null.

String attributes = null;
if (execution.getAttributes() != null && !execution.getAttributes().isEmpty()) {
    attributes = JSON.writeValueAsString(execution.getAttributes());
}

Note: IngestionService has a static private static final ObjectMapper JSON field (line 22). Use JSON.writeValueAsString().

  • Step 2: Extract attributes in IngestionService.flattenProcessors()

In the flattenProcessors() method (~line 113-138), serialize each ProcessorExecution.getAttributes() to JSON string. Pass as the new attributes parameter to ProcessorRecord.

  • Step 3: Update PostgresExecutionStore.upsert()

Add attributes to the INSERT statement and bind parameters. The column is JSONB, so use PGobject with type "jsonb" or cast ?::jsonb in the SQL.

In the INSERT (~line 26-32): add attributes column and ?::jsonb placeholder. In the ON CONFLICT UPDATE (~line 33-51): add attributes = COALESCE(EXCLUDED.attributes, executions.attributes) merge (follows the existing pattern, e.g., input_body = COALESCE(EXCLUDED.input_body, executions.input_body)). In the bind parameters (~line 53-62): bind record.attributes().

  • Step 4: Update PostgresExecutionStore.upsertProcessors()

Same pattern: add attributes column, ?::jsonb placeholder, bind parameter.

  • Step 5: Verify compilation

Run: mvn compile -q Expected: BUILD SUCCESS (or remaining errors from DetailService/SearchIndexer which are next tasks)

  • Step 6: Commit
git add cameleer3-server-core/src/main/java/com/cameleer3/server/core/ingestion/IngestionService.java
git add cameleer3-server-app/src/main/java/com/cameleer3/server/app/storage/PostgresExecutionStore.java
git commit -m "feat: store execution and processor attributes from agent data"

Task 4: Backend — Attributes in Detail Service and OpenSearch Indexing

Files:

  • Modify: cameleer3-server-core/src/main/java/com/cameleer3/server/core/detail/DetailService.java

  • Modify: cameleer3-server-core/src/main/java/com/cameleer3/server/core/storage/model/ExecutionDocument.java

  • Modify: cameleer3-server-core/src/main/java/com/cameleer3/server/core/indexing/SearchIndexer.java

  • Modify: cameleer3-server-app/src/main/java/com/cameleer3/server/app/search/OpenSearchIndex.java

  • Step 1: Pass attributes through DetailService.buildTree()

In buildTree() (~line 35-63), when constructing ProcessorNode from ProcessorRecord, deserialize the attributes JSON string back to Map<String, String> and pass it to the constructor.

In getDetail() (~line 16-33), when constructing ExecutionDetail, deserialize the ExecutionRecord.attributes() JSON and pass it as the attributes parameter.

  • Step 2: Update PostgresExecutionStore.findById() and findProcessors() queries

These SELECT queries need to include the new attributes column and map it into ExecutionRecord / ProcessorRecord via the row mapper.

  • Step 3: Add attributes to ExecutionDocument.ProcessorDoc

Add String attributes field to the ProcessorDoc record in ExecutionDocument.java. Also add String attributes to ExecutionDocument itself for route-level attributes.

  • Step 4: Update SearchIndexer.indexExecution()

When constructing ProcessorDoc objects (~line 68-74), pass processor.attributes(). When constructing ExecutionDocument (~line 76-80), pass the execution record's attributes.

  • Step 5: Update OpenSearchIndex.toMap()

In the toMap() method (~line 303-333), add "attributes" to the document map and to each processor sub-document map.

  • Step 6: Update OpenSearchIndex.fromSearchHit() (or equivalent)

When parsing search results back into ExecutionSummary, extract the attributes field from the OpenSearch hit source and deserialize it into Map<String, String>.

  • Step 7: Verify compilation

Run: mvn compile -q Expected: BUILD SUCCESS

  • Step 8: Commit
git add cameleer3-server-core/ cameleer3-server-app/
git commit -m "feat: thread attributes through detail service and OpenSearch indexing"

Task 5: Backend — TEST_EXPRESSION Command and Request-Reply Infrastructure

Files:

  • Modify: cameleer3-server-core/src/main/java/com/cameleer3/server/core/agent/CommandType.java

  • Modify: cameleer3-server-core/src/main/java/com/cameleer3/server/core/agent/AgentRegistryService.java

  • Modify: cameleer3-server-app/src/main/java/com/cameleer3/server/app/dto/CommandAckRequest.java

  • Modify: cameleer3-server-app/src/main/java/com/cameleer3/server/app/controller/AgentCommandController.java

  • Create: cameleer3-server-app/src/main/java/com/cameleer3/server/app/dto/TestExpressionRequest.java

  • Create: cameleer3-server-app/src/main/java/com/cameleer3/server/app/dto/TestExpressionResponse.java

  • Modify: cameleer3-server-app/src/main/java/com/cameleer3/server/app/controller/ApplicationConfigController.java

  • Step 1: Add TEST_EXPRESSION to CommandType enum

public enum CommandType {
    CONFIG_UPDATE,
    DEEP_TRACE,
    REPLAY,
    SET_TRACED_PROCESSORS,
    TEST_EXPRESSION
}
  • Step 2: Add data field to CommandAckRequest
public record CommandAckRequest(String status, String message, String data) {}

The data field carries structured JSON results (e.g., expression test result). Existing ACKs that don't send data will deserialize data as null.

  • Step 3: Add CompletableFuture map to AgentRegistryService

Add a ConcurrentHashMap<String, CompletableFuture<CommandAckRequest>> for pending request-reply commands. Add methods:

public CompletableFuture<CommandAckRequest> addCommandWithReply(String agentId, CommandType type, String payload) {
    AgentCommand command = addCommand(agentId, type, payload);
    CompletableFuture<CommandAckRequest> future = new CompletableFuture<>();
    pendingReplies.put(command.id(), future);
    return future;
}

public void completeReply(String commandId, CommandAckRequest ack) {
    CompletableFuture<CommandAckRequest> future = pendingReplies.remove(commandId);
    if (future != null) {
        future.complete(ack);
    }
}

Note: Use future.orTimeout(5, TimeUnit.SECONDS) in the caller. The future auto-completes exceptionally on timeout. Add a whenComplete handler that removes the entry from pendingReplies to prevent leaks:

future.whenComplete((result, ex) -> pendingReplies.remove(command.id()));
  • Step 4: Complete futures in AgentCommandController.acknowledgeCommand()

In the ACK endpoint (~line 156-179), after registryService.acknowledgeCommand(), call registryService.completeReply(commandId, ack).

  • Step 5: Add test-expression mapping to mapCommandType()
case "test-expression" -> CommandType.TEST_EXPRESSION;
  • Step 6: Create TestExpressionRequest and TestExpressionResponse DTOs
// TestExpressionRequest.java
public record TestExpressionRequest(String expression, String language, String body, String target) {}

// TestExpressionResponse.java
public record TestExpressionResponse(String result, String error) {}
  • Step 7: Add test-expression endpoint to ApplicationConfigController

Note: ApplicationConfigController does not use @PreAuthorize — security is handled at the URL pattern level in the security config. The test-expression endpoint inherits the same access rules as other config endpoints. No @PreAuthorize annotation needed.

@PostMapping("/{application}/test-expression")
@Operation(summary = "Test a tap expression against sample data via a live agent")
public ResponseEntity<TestExpressionResponse> testExpression(
        @PathVariable String application,
        @RequestBody TestExpressionRequest request) {
    // 1. Find a LIVE agent for this application via registryService
    // 2. Send TEST_EXPRESSION command with addCommandWithReply()
    // 3. Await CompletableFuture with 5s timeout via future.orTimeout(5, TimeUnit.SECONDS)
    // 4. Parse ACK data as result/error, return TestExpressionResponse
    // Handle: no live agent (404), timeout (504), parse error (500)
    // Clean up: future.whenComplete removes from pendingReplies map on timeout
}
  • Step 8: Verify compilation

Run: mvn compile -q Expected: BUILD SUCCESS

  • Step 9: Commit
git add cameleer3-server-core/ cameleer3-server-app/
git commit -m "feat: add TEST_EXPRESSION command with request-reply infrastructure"

Task 6: Backend — Regenerate OpenAPI and Schema

Files:

  • Modify: openapi.json (regenerated)

  • Modify: ui/src/api/schema.d.ts (regenerated)

  • Step 1: Build the server to generate updated OpenAPI spec

Run: mvn clean compile -q

  • Step 2: Start the server temporarily to extract OpenAPI JSON

Run the server, fetch http://localhost:8080/v3/api-docs, save to openapi.json. Alternatively, if the project has an automated OpenAPI generation step, use that.

  • Step 3: Regenerate schema.d.ts from openapi.json

Run the existing schema generation command (check package.json scripts in ui/).

  • Step 4: Verify the new types include attributes on ExecutionDetail, ProcessorNode, ExecutionSummary

Read ui/src/api/schema.d.ts and confirm the fields are present. Note: the OpenAPI generator may strip nullable fields (e.g., highlight exists on Java ExecutionSummary but not in the current schema). If attributes is missing, add @Schema(nullable = true) or @JsonInclude(JsonInclude.Include.ALWAYS) annotation on the Java DTO and regenerate. Alternatively, manually add the field to schema.d.ts.

  • Step 5: Commit
git add openapi.json ui/src/api/schema.d.ts
git commit -m "chore: regenerate openapi.json and schema.d.ts with attributes and test-expression"

Task 7: Frontend — TypeScript Types and API Hooks

Files:

  • Modify: ui/src/api/queries/commands.ts

  • Step 1: Add TapDefinition interface

export interface TapDefinition {
  tapId: string;
  processorId: string;
  target: 'INPUT' | 'OUTPUT' | 'BOTH';
  expression: string;
  language: string;
  attributeName: string;
  attributeType: 'BUSINESS_OBJECT' | 'CORRELATION' | 'EVENT' | 'CUSTOM';
  enabled: boolean;
  version: number;
}
  • Step 2: Extend ApplicationConfig interface

Add to the existing ApplicationConfig interface:

taps: TapDefinition[];
tapVersion: number;
routeRecording: Record<string, boolean>;
compressSuccess: boolean;
  • Step 3: Add useTestExpression mutation hook
export function useTestExpression() {
  return useMutation({
    mutationFn: async ({ application, expression, language, body, target }: {
      application: string;
      expression: string;
      language: string;
      body: string;
      target: string;
    }) => {
      const { data, error } = await api.POST('/config/{application}/test-expression', {
        params: { path: { application } },
        body: { expression, language, body, target },
      });
      if (error) throw new Error('Failed to test expression');
      return data!;
    },
  });
}
  • Step 4: Add useReplayExchange mutation hook
export function useReplayExchange() {
  return useMutation({
    mutationFn: async ({ agentId, headers, body }: {
      agentId: string;
      headers: Record<string, string>;
      body: string;
    }) => {
      const { data, error } = await api.POST('/agents/{id}/commands', {
        params: { path: { id: agentId } },
        body: { type: 'replay', payload: { headers, body } } as any,
      });
      if (error) throw new Error('Failed to send replay command');
      return data!;
    },
  });
}
  • Step 5: Verify build

Run: cd ui && npm run build Expected: BUILD SUCCESS (or type errors in pages that now receive new fields — those pages are updated in later tasks)

  • Step 6: Commit
git add ui/src/api/queries/commands.ts
git commit -m "feat: add TapDefinition type, extend ApplicationConfig, add test-expression and replay hooks"

Task 8: Frontend — Business Attributes on ExchangeDetail

Files:

  • Modify: ui/src/pages/ExchangeDetail/ExchangeDetail.tsx

  • Modify: ui/src/pages/ExchangeDetail/ExchangeDetail.module.css

  • Step 1: Add attributes strip to exchange header

After the header info row and before the stat boxes, render the route-level attributes:

{detail.attributes && Object.keys(detail.attributes).length > 0 && (
  <div className={styles.attributesStrip}>
    <span className={styles.attributesLabel}>Attributes</span>
    {Object.entries(detail.attributes).map(([key, value]) => (
      <Badge key={key} label={`${key}: ${value}`} color="auto" variant="filled" />
    ))}
  </div>
)}
  • Step 2: Add per-processor attributes in processor detail panel

In the processor detail section (where the selected processor's message IN/OUT is shown), add attributes badges if the selected processor has them. Access via detail.processors tree — traverse the nested tree to find the processor at the selected index and read its attributes map. Note: body/headers data comes from a separate useProcessorSnapshot call, but attributes is inline on the ProcessorNode in the detail response — no additional API call needed.

  • Step 3: Add CSS for attributes strip
.attributesStrip {
  display: flex;
  gap: 8px;
  flex-wrap: wrap;
  align-items: center;
  padding: 10px 14px;
  background: var(--bg-surface);
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius-lg);
  margin-bottom: 16px;
}

.attributesLabel {
  font-size: 11px;
  color: var(--text-muted);
  margin-right: 4px;
}
  • Step 4: Verify build

Run: cd ui && npm run build Expected: BUILD SUCCESS

  • Step 5: Commit
git add ui/src/pages/ExchangeDetail/
git commit -m "feat: display business attributes on ExchangeDetail page"

Task 9: Frontend — Replay Modal on ExchangeDetail

Files:

  • Modify: ui/src/pages/ExchangeDetail/ExchangeDetail.tsx

  • Modify: ui/src/pages/ExchangeDetail/ExchangeDetail.module.css

  • Step 1: Add replay button to exchange header

Add a "Replay" button (primary variant) in the header action area. Only render for OPERATOR/ADMIN roles (check with useAuthStore()).

<Button variant="primary" size="sm" onClick={() => setReplayOpen(true)}>
   Replay
</Button>
  • Step 2: Build the replay modal component

Add state: replayOpen, replayHeaders (key-value array), replayBody (string), replayAgent (string), replayTab ('headers' | 'body').

Pre-populate from detail.inputHeaders (parse JSON string to object) and detail.inputBody.

Use Modal (size="lg"), Tabs for Headers/Body, and the useReplayExchange mutation hook.

Headers tab: render editable rows with Input fields for key and value, remove button per row, "Add header" link at bottom.

Body tab: Textarea with monospace font, pre-populated with detail.inputBody.

  • Step 3: Wire up agent selector

Use useAgents('LIVE', detail.applicationName) to populate a Select dropdown. Default to the agent that originally processed this exchange (detail.agentId) if it's still LIVE.

  • Step 4: Wire up replay submission

On "Replay" click: call replayExchange.mutate({ agentId, headers, body }). Show loading spinner on button. On success: toast('Replay command sent'), close modal. On error: toast('Replay failed: ...').

  • Step 5: Add CSS for replay modal elements

Style the warning banner, header table, body textarea, and agent selector.

  • Step 6: Verify build

Run: cd ui && npm run build Expected: BUILD SUCCESS

  • Step 7: Commit
git add ui/src/pages/ExchangeDetail/
git commit -m "feat: add replay modal with editable headers and body on ExchangeDetail"

Task 10: Frontend — Attributes Column on Dashboard

Files:

  • Modify: ui/src/pages/Dashboard/Dashboard.tsx

  • Step 1: Add attributes column to the exchanges table

In buildBaseColumns() (~line 97-163), add a new column after the applicationName column. Use CSS module classes (not inline styles — per project convention in feedback_css_modules_not_inline.md):

{
  key: 'attributes',
  header: 'Attributes',
  render: (_, row) => {
    const attrs = row.attributes;
    if (!attrs || Object.keys(attrs).length === 0) return <span className={styles.muted}></span>;
    const entries = Object.entries(attrs);
    const shown = entries.slice(0, 2);
    const overflow = entries.length - 2;
    return (
      <div className={styles.attrCell}>
        {shown.map(([k, v]) => (
          <Badge key={k} label={String(v)} color="auto" title={k} />
        ))}
        {overflow > 0 && <span className={styles.attrOverflow}>+{overflow}</span>}
      </div>
    );
  },
},

Add corresponding CSS classes to Dashboard.module.css:

.attrCell { display: flex; gap: 4px; align-items: center; }
.attrOverflow { font-size: 10px; color: var(--text-muted); }
  • Step 2: Verify build

Run: cd ui && npm run build Expected: BUILD SUCCESS

  • Step 3: Commit
git add ui/src/pages/Dashboard/Dashboard.tsx
git commit -m "feat: show business attributes as compact badges in dashboard exchanges table"

Task 11: Frontend — RouteDetail Recording Toggle and Taps KPI

Files:

  • Modify: ui/src/pages/Routes/RouteDetail.tsx

  • Modify: ui/src/pages/Routes/RouteDetail.module.css

  • Step 1: Add recording toggle to route header

Add imports: import { useApplicationConfig, useUpdateApplicationConfig } from '../../api/queries/commands' and Toggle from @cameleer/design-system.

In the route header section, add a pill-styled container with a Toggle component:

const config = useApplicationConfig(appId);
const updateConfig = useUpdateApplicationConfig();

const isRecording = config.data?.routeRecording?.[routeId] !== false; // default true

function toggleRecording() {
  if (!config.data) return;
  const routeRecording = { ...config.data.routeRecording, [routeId]: !isRecording };
  updateConfig.mutate({ ...config.data, routeRecording });
}

Render:

<div className={styles.recordingPill}>
  <span className={styles.recordingLabel}>Recording</span>
  <Toggle checked={isRecording} onChange={toggleRecording} />
</div>
  • Step 2: Add "Active Taps" to KPI strip

Count enabled taps for this route's processors (cross-reference tap processorIds with this route's processor list from diagram data). Add to kpiItems array.

  • Step 3: Add "Taps" tab to tabs array
const tapCount = /* count taps for this route */;
const tabs = [
  { label: 'Performance', value: 'performance' },
  { label: 'Recent Executions', value: 'executions', count: exchangeRows.length },
  { label: 'Error Patterns', value: 'errors', count: errorPatterns.length },
  { label: 'Taps', value: 'taps', count: tapCount },
];
  • Step 4: Add CSS for recording pill
.recordingPill {
  display: flex;
  align-items: center;
  gap: 8px;
  background: var(--bg-surface);
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius-lg);
  padding: 6px 12px;
}

.recordingLabel {
  font-size: 11px;
  color: var(--text-muted);
}
  • Step 5: Verify build

Run: cd ui && npm run build Expected: BUILD SUCCESS

  • Step 6: Commit
git add ui/src/pages/Routes/
git commit -m "feat: add recording toggle and taps KPI to RouteDetail header"

Task 12: Frontend — RouteDetail Taps Tab and Tap Modal

Files:

  • Modify: ui/src/pages/Routes/RouteDetail.tsx

  • Modify: ui/src/pages/Routes/RouteDetail.module.css

  • Step 1: Render taps DataTable when "Taps" tab is active

Filter config.data.taps to only taps whose processorId exists in this route's diagram. Display in a DataTable with columns: Attribute, Processor, Expression, Language, Target, Type, Enabled (Toggle), Actions.

Empty state: "No taps configured for this route. Add a tap to extract business attributes from exchange data."

  • Step 2: Build the Add/Edit Tap modal

State: tapModalOpen, editingTap (null for new, TapDefinition for edit), form fields.

Modal contents:

  • FormField + Input for Attribute Name

  • FormField + Select for Processor (options from useDiagramLayout node list)

  • Two FormFields side-by-side: Select for Language (simple, jsonpath, xpath, jq, groovy) and Select for Target (INPUT, OUTPUT, BOTH)

  • FormField + Textarea for Expression (monospace)

  • Attribute Type pill selector (4 options, styled as button group)

  • Toggle for Enabled

  • Step 3: Add Test Expression section to tap modal

Collapsible section (default expanded) with two tabs: "Recent Exchange" and "Custom Payload".

Recent Exchange tab:

  • Use useSearchExecutions with this route's filter to get recent exchanges as summaries
  • Auto-select most recent exchange, then fetch its detail via useExecutionDetail to get the inputBody for the test payload
  • Select dropdown to change exchange
  • "Test" button calls useTestExpression mutation with the exchange's body

Custom Payload tab:

  • Textarea pre-populated from the most recent exchange's body (fetched via detail endpoint)
  • Switching from Recent Exchange tab carries the payload over
  • "Test" button calls useTestExpression mutation

Result display: green box for success, red box for error.

  • Step 4: Wire up tap save

On save: update the taps array in ApplicationConfig (add new or replace existing by tapId), then call updateConfig.mutate(). Generate tapId as UUID for new taps.

  • Step 5: Wire up tap delete

On delete: remove tap from array, call updateConfig.mutate(). Import and use ConfirmDialog from @cameleer/design-system before deleting.

  • Step 6: Wire up enabled toggle inline

Toggle in the DataTable row directly calls config update (toggle the specific tap's enabled field).

  • Step 7: Add CSS for taps tab content

Style the taps header (title + button), tap modal form layout, test expression section, result boxes.

  • Step 8: Verify build

Run: cd ui && npm run build Expected: BUILD SUCCESS

  • Step 9: Commit
git add ui/src/pages/Routes/
git commit -m "feat: add taps management tab with CRUD modal and expression testing on RouteDetail"

Task 13: Frontend — AppConfigDetailPage Restructure

Files:

  • Modify: ui/src/pages/Admin/AppConfigDetailPage.tsx

  • Modify: ui/src/pages/Admin/AppConfigDetailPage.module.css

  • Step 1: Merge Logging + Observability into "Settings" section

Replace the two separate SectionHeader sections with a single "Settings" section. Render all setting badges in a single flex row: Log Forwarding, Engine Level, Payload Capture, Metrics, Sampling Rate, Compress Success (new field).

Edit mode: all badges become dropdowns/toggles as before, plus a new Toggle for compressSuccess.

  • Step 2: Merge Traced Processors + Taps into "Traces & Taps" section

Build a merged data structure: for each processor that has either a trace override or taps, create a row with Route, Processor, Capture badge, Taps badges.

To resolve processor-to-route mapping: fetch route catalog for this application, then for each route fetch its diagram. Build a Map<processorId, routeId> by iterating diagram nodes. For processors not found, show "unknown".

Table columns: Route, Processor, Capture (badge/select in edit mode), Taps (attribute badges with enabled indicators, read-only).

Summary: "N traced · M taps · manage taps on route pages".

  • Step 3: Add "Route Recording" section

Fetch route list from useRouteCatalog filtered by application. Render table with Route name and Toggle.

In view mode: toggles show current state (disabled). In edit mode: toggles are interactive.

Default for routes not in routeRecording map: recording enabled (true).

Summary: "N of M routes recording".

  • Step 4: Update form state for new fields

Add compressSuccess and routeRecording to the form state object and updateField handler. Ensure save sends the complete config including new fields.

  • Step 5: Update CSS for restructured sections

Adjust section spacing, flex row for merged settings badges.

  • Step 6: Verify build

Run: cd ui && npm run build Expected: BUILD SUCCESS

  • Step 7: Commit
git add ui/src/pages/Admin/
git commit -m "feat: restructure AppConfigDetailPage to Settings, Traces & Taps, Route Recording sections"

Task 14: Final Build Verification and Push

  • Step 1: Run full backend build

Run: mvn clean compile -q Expected: BUILD SUCCESS

  • Step 2: Run full frontend build

Run: cd ui && npm run build Expected: BUILD SUCCESS

  • Step 3: Manual smoke test checklist

Verify in browser:

  • ExchangeDetail shows attributes strip when attributes exist

  • ExchangeDetail replay button opens modal, can send replay

  • Dashboard table shows attributes column

  • RouteDetail shows recording toggle, taps tab with CRUD

  • Tap modal test expression section works (if live agent available)

  • AppConfigDetailPage shows 3 merged sections

  • AppConfigDetailPage edit mode works for compress success and route recording

  • Step 4: Push to remote

git push origin main