# 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.
- [ ]**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 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`.
```java
String attributes = null;
if (execution.getAttributes() != null && !execution.getAttributes().isEmpty()) {
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 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.
When parsing search results back into `ExecutionSummary`, extract the `attributes` field from the OpenSearch hit source and deserialize it into `Map<String, String>`.
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:
- [ ]**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()**
```java
case "test-expression" -> CommandType.TEST_EXPRESSION;
```
- [ ]**Step 6: Create TestExpressionRequest and TestExpressionResponse DTOs**
```java
// 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.
```java
@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
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**
```bash
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
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**
```css
.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**
```bash
git add ui/src/pages/ExchangeDetail/
git commit -m "feat: display business attributes on ExchangeDetail page"
```
---
## Task 9: Frontend — Replay Modal on ExchangeDetail
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**
```bash
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`):
```typescript
{
key: 'attributes',
header: 'Attributes',
render: (_, row) => {
const attrs = row.attributes;
if (!attrs || Object.keys(attrs).length === 0) return <span className={styles.muted}>—</span>;
- [ ]**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:
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 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
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**
```bash
git add ui/src/pages/Routes/
git commit -m "feat: add taps management tab with CRUD modal and expression testing on RouteDetail"
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`.
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.