refactor: rename group/groupName to application/applicationName
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

The execution-related "group" concept actually represents the
application name. Rename all Java fields, API parameters, and frontend
types from groupName→applicationName and group→application for clarity.

- Java records: ExecutionSummary, ExecutionDetail, ExecutionDocument,
  ExecutionRecord, ProcessorRecord
- API params: SearchRequest.group→application, SearchController
  @RequestParam group→application
- Services: IngestionService, DetailService, SearchIndexer, StatsStore
- Frontend: schema.d.ts, Dashboard, ExchangeDetail, RouteDetail,
  executions query hooks

Database column names (group_name) and OpenSearch field names are
unchanged — only the API-facing Java/TS field names are renamed.

RBAC group references (groups table, GroupRepository, GroupsTab) are
a separate domain concept and are NOT affected by this change.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
hsiegeln
2026-03-23 21:21:38 +01:00
parent 3c226de62f
commit 8ad0016a8e
54 changed files with 21442 additions and 73 deletions

View File

@@ -53,11 +53,11 @@ public class ExecutionController {
@ApiResponse(responseCode = "202", description = "Data accepted for processing") @ApiResponse(responseCode = "202", description = "Data accepted for processing")
public ResponseEntity<Void> ingestExecutions(@RequestBody String body) throws JsonProcessingException { public ResponseEntity<Void> ingestExecutions(@RequestBody String body) throws JsonProcessingException {
String agentId = extractAgentId(); String agentId = extractAgentId();
String groupName = resolveGroupName(agentId); String applicationName = resolveApplicationName(agentId);
List<RouteExecution> executions = parsePayload(body); List<RouteExecution> executions = parsePayload(body);
for (RouteExecution execution : executions) { for (RouteExecution execution : executions) {
ingestionService.ingestExecution(agentId, groupName, execution); ingestionService.ingestExecution(agentId, applicationName, execution);
} }
return ResponseEntity.accepted().build(); return ResponseEntity.accepted().build();
@@ -68,7 +68,7 @@ public class ExecutionController {
return auth != null ? auth.getName() : ""; return auth != null ? auth.getName() : "";
} }
private String resolveGroupName(String agentId) { private String resolveApplicationName(String agentId) {
AgentInfo agent = registryService.findById(agentId); AgentInfo agent = registryService.findById(agentId);
return agent != null ? agent.group() : ""; return agent != null ? agent.group() : "";
} }

View File

@@ -65,7 +65,7 @@ public class RouteMetricsController {
List<RouteKey> routeKeys = new ArrayList<>(); List<RouteKey> routeKeys = new ArrayList<>();
List<RouteMetrics> metrics = jdbc.query(sql.toString(), (rs, rowNum) -> { List<RouteMetrics> metrics = jdbc.query(sql.toString(), (rs, rowNum) -> {
String groupName = rs.getString("group_name"); String applicationName = rs.getString("group_name");
String routeId = rs.getString("route_id"); String routeId = rs.getString("route_id");
long total = rs.getLong("total"); long total = rs.getLong("total");
long failed = rs.getLong("failed"); long failed = rs.getLong("failed");
@@ -76,8 +76,8 @@ public class RouteMetricsController {
double errorRate = total > 0 ? (double) failed / total : 0.0; double errorRate = total > 0 ? (double) failed / total : 0.0;
double tps = windowSeconds > 0 ? (double) total / windowSeconds : 0.0; double tps = windowSeconds > 0 ? (double) total / windowSeconds : 0.0;
routeKeys.add(new RouteKey(groupName, routeId)); routeKeys.add(new RouteKey(applicationName, routeId));
return new RouteMetrics(routeId, groupName, total, successRate, return new RouteMetrics(routeId, applicationName, total, successRate,
avgDur, p99Dur, errorRate, tps, List.of()); avgDur, p99Dur, errorRate, tps, List.of());
}, params.toArray()); }, params.toArray());

View File

@@ -51,13 +51,13 @@ public class SearchController {
@RequestParam(required = false) String routeId, @RequestParam(required = false) String routeId,
@RequestParam(required = false) String agentId, @RequestParam(required = false) String agentId,
@RequestParam(required = false) String processorType, @RequestParam(required = false) String processorType,
@RequestParam(required = false) String group, @RequestParam(required = false) String application,
@RequestParam(defaultValue = "0") int offset, @RequestParam(defaultValue = "0") int offset,
@RequestParam(defaultValue = "50") int limit, @RequestParam(defaultValue = "50") int limit,
@RequestParam(required = false) String sortField, @RequestParam(required = false) String sortField,
@RequestParam(required = false) String sortDir) { @RequestParam(required = false) String sortDir) {
List<String> agentIds = resolveGroupToAgentIds(group); List<String> agentIds = resolveApplicationToAgentIds(application);
SearchRequest request = new SearchRequest( SearchRequest request = new SearchRequest(
status, timeFrom, timeTo, status, timeFrom, timeTo,
@@ -65,7 +65,7 @@ public class SearchController {
correlationId, correlationId,
text, null, null, null, text, null, null, null,
routeId, agentId, processorType, routeId, agentId, processorType,
group, agentIds, application, agentIds,
offset, limit, offset, limit,
sortField, sortDir sortField, sortDir
); );
@@ -77,11 +77,11 @@ public class SearchController {
@Operation(summary = "Advanced search with all filters") @Operation(summary = "Advanced search with all filters")
public ResponseEntity<SearchResult<ExecutionSummary>> searchPost( public ResponseEntity<SearchResult<ExecutionSummary>> searchPost(
@RequestBody SearchRequest request) { @RequestBody SearchRequest request) {
// Resolve group to agentIds if group is specified but agentIds is not // Resolve application to agentIds if application is specified but agentIds is not
SearchRequest resolved = request; SearchRequest resolved = request;
if (request.group() != null && !request.group().isBlank() if (request.application() != null && !request.application().isBlank()
&& (request.agentIds() == null || request.agentIds().isEmpty())) { && (request.agentIds() == null || request.agentIds().isEmpty())) {
resolved = request.withAgentIds(resolveGroupToAgentIds(request.group())); resolved = request.withAgentIds(resolveApplicationToAgentIds(request.application()));
} }
return ResponseEntity.ok(searchService.search(resolved)); return ResponseEntity.ok(searchService.search(resolved));
} }
@@ -92,9 +92,9 @@ public class SearchController {
@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 group) { @RequestParam(required = false) String application) {
Instant end = to != null ? to : Instant.now(); Instant end = to != null ? to : Instant.now();
List<String> agentIds = resolveGroupToAgentIds(group); List<String> agentIds = resolveApplicationToAgentIds(application);
if (routeId == null && agentIds == null) { if (routeId == null && agentIds == null) {
return ResponseEntity.ok(searchService.stats(from, end)); return ResponseEntity.ok(searchService.stats(from, end));
} }
@@ -108,9 +108,9 @@ public class SearchController {
@RequestParam(required = false) Instant to, @RequestParam(required = false) Instant to,
@RequestParam(defaultValue = "24") int buckets, @RequestParam(defaultValue = "24") int buckets,
@RequestParam(required = false) String routeId, @RequestParam(required = false) String routeId,
@RequestParam(required = false) String group) { @RequestParam(required = false) String application) {
Instant end = to != null ? to : Instant.now(); Instant end = to != null ? to : Instant.now();
List<String> agentIds = resolveGroupToAgentIds(group); List<String> agentIds = resolveApplicationToAgentIds(application);
if (routeId == null && agentIds == null) { if (routeId == null && agentIds == null) {
return ResponseEntity.ok(searchService.timeseries(from, end, buckets)); return ResponseEntity.ok(searchService.timeseries(from, end, buckets));
} }
@@ -118,14 +118,14 @@ public class SearchController {
} }
/** /**
* Resolve an application group name to agent IDs. * Resolve an application name to agent IDs.
* Returns null if group is null/blank (no filtering). * Returns null if application is null/blank (no filtering).
*/ */
private List<String> resolveGroupToAgentIds(String group) { private List<String> resolveApplicationToAgentIds(String application) {
if (group == null || group.isBlank()) { if (application == null || application.isBlank()) {
return null; return null;
} }
return registryService.findByGroup(group).stream() return registryService.findByGroup(application).stream()
.map(AgentInfo::id) .map(AgentInfo::id)
.toList(); .toList();
} }

View File

@@ -288,7 +288,7 @@ public class OpenSearchIndex implements SearchIndex {
map.put("execution_id", doc.executionId()); map.put("execution_id", doc.executionId());
map.put("route_id", doc.routeId()); map.put("route_id", doc.routeId());
map.put("agent_id", doc.agentId()); map.put("agent_id", doc.agentId());
map.put("group_name", doc.groupName()); map.put("group_name", doc.applicationName());
map.put("status", doc.status()); map.put("status", doc.status());
map.put("correlation_id", doc.correlationId()); map.put("correlation_id", doc.correlationId());
map.put("exchange_id", doc.exchangeId()); map.put("exchange_id", doc.exchangeId());

View File

@@ -45,7 +45,7 @@ public class PostgresExecutionStore implements ExecutionStore {
updated_at = now() updated_at = now()
""", """,
execution.executionId(), execution.routeId(), execution.agentId(), execution.executionId(), execution.routeId(), execution.agentId(),
execution.groupName(), execution.status(), execution.correlationId(), execution.applicationName(), execution.status(), execution.correlationId(),
execution.exchangeId(), execution.exchangeId(),
Timestamp.from(execution.startTime()), Timestamp.from(execution.startTime()),
execution.endTime() != null ? Timestamp.from(execution.endTime()) : null, execution.endTime() != null ? Timestamp.from(execution.endTime()) : null,
@@ -55,7 +55,7 @@ public class PostgresExecutionStore implements ExecutionStore {
@Override @Override
public void upsertProcessors(String executionId, Instant startTime, public void upsertProcessors(String executionId, Instant startTime,
String groupName, String routeId, String applicationName, String routeId,
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,
@@ -76,7 +76,7 @@ public class PostgresExecutionStore implements ExecutionStore {
""", """,
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.groupName(), p.routeId(), p.diagramNodeId(), 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,

View File

@@ -29,9 +29,9 @@ public class PostgresStatsStore implements StatsStore {
} }
@Override @Override
public ExecutionStats statsForApp(Instant from, Instant to, String groupName) { public ExecutionStats statsForApp(Instant from, Instant to, String applicationName) {
return queryStats("stats_1m_app", from, to, List.of( return queryStats("stats_1m_app", from, to, List.of(
new Filter("group_name", groupName))); new Filter("group_name", applicationName)));
} }
@Override @Override
@@ -56,9 +56,9 @@ public class PostgresStatsStore implements StatsStore {
} }
@Override @Override
public StatsTimeseries timeseriesForApp(Instant from, Instant to, int bucketCount, String groupName) { public StatsTimeseries timeseriesForApp(Instant from, Instant to, int bucketCount, String applicationName) {
return queryTimeseries("stats_1m_app", from, to, bucketCount, List.of( return queryTimeseries("stats_1m_app", from, to, bucketCount, List.of(
new Filter("group_name", groupName)), true); new Filter("group_name", applicationName)), true);
} }
@Override @Override

View File

@@ -54,10 +54,10 @@ class PostgresStatsStoreIT extends AbstractPostgresIT {
assertFalse(ts.buckets().isEmpty()); assertFalse(ts.buckets().isEmpty());
} }
private void insertExecution(String id, String routeId, String groupName, private void insertExecution(String id, String routeId, String applicationName,
String status, Instant startTime, long durationMs) { String status, Instant startTime, long durationMs) {
executionStore.upsert(new ExecutionRecord( executionStore.upsert(new ExecutionRecord(
id, routeId, "agent-1", groupName, status, null, null, id, routeId, "agent-1", applicationName, status, null, null,
startTime, startTime.plusMillis(durationMs), durationMs, startTime, startTime.plusMillis(durationMs), durationMs,
status.equals("FAILED") ? "error" : null, null, null)); status.equals("FAILED") ? "error" : null, null, null));
} }

View File

@@ -20,7 +20,7 @@ public class DetailService {
List<ProcessorNode> roots = buildTree(processors); List<ProcessorNode> roots = buildTree(processors);
return new ExecutionDetail( return new ExecutionDetail(
exec.executionId(), exec.routeId(), exec.agentId(), exec.executionId(), exec.routeId(), exec.agentId(),
exec.groupName(), exec.applicationName(),
exec.status(), exec.startTime(), exec.endTime(), exec.status(), exec.startTime(), exec.endTime(),
exec.durationMs() != null ? exec.durationMs() : 0L, exec.durationMs() != null ? exec.durationMs() : 0L,
exec.correlationId(), exec.exchangeId(), exec.correlationId(), exec.exchangeId(),

View File

@@ -27,7 +27,7 @@ public record ExecutionDetail(
String executionId, String executionId,
String routeId, String routeId,
String agentId, String agentId,
String groupName, String applicationName,
String status, String status,
Instant startTime, Instant startTime,
Instant endTime, Instant endTime,

View File

@@ -74,7 +74,7 @@ public class SearchIndexer implements SearchIndexerStats {
.toList(); .toList();
searchIndex.index(new ExecutionDocument( searchIndex.index(new ExecutionDocument(
exec.executionId(), exec.routeId(), exec.agentId(), exec.groupName(), exec.executionId(), exec.routeId(), exec.agentId(), exec.applicationName(),
exec.status(), exec.correlationId(), exec.exchangeId(), exec.status(), exec.correlationId(), exec.exchangeId(),
exec.startTime(), exec.endTime(), exec.durationMs(), exec.startTime(), exec.endTime(), exec.durationMs(),
exec.errorMessage(), exec.errorStacktrace(), processorDocs)); exec.errorMessage(), exec.errorStacktrace(), processorDocs));

View File

@@ -38,18 +38,18 @@ public class IngestionService {
this.bodySizeLimit = bodySizeLimit; this.bodySizeLimit = bodySizeLimit;
} }
public void ingestExecution(String agentId, String groupName, RouteExecution execution) { public void ingestExecution(String agentId, String applicationName, RouteExecution execution) {
ExecutionRecord record = toExecutionRecord(agentId, groupName, execution); ExecutionRecord record = toExecutionRecord(agentId, applicationName, execution);
executionStore.upsert(record); executionStore.upsert(record);
if (execution.getProcessors() != null && !execution.getProcessors().isEmpty()) { if (execution.getProcessors() != null && !execution.getProcessors().isEmpty()) {
List<ProcessorRecord> processors = flattenProcessors( List<ProcessorRecord> processors = flattenProcessors(
execution.getProcessors(), record.executionId(), execution.getProcessors(), record.executionId(),
record.startTime(), groupName, execution.getRouteId(), record.startTime(), applicationName, execution.getRouteId(),
null, 0); null, 0);
executionStore.upsertProcessors( executionStore.upsertProcessors(
record.executionId(), record.startTime(), record.executionId(), record.startTime(),
groupName, execution.getRouteId(), processors); applicationName, execution.getRouteId(), processors);
} }
eventPublisher.accept(new ExecutionUpdatedEvent( eventPublisher.accept(new ExecutionUpdatedEvent(
@@ -72,13 +72,13 @@ public class IngestionService {
return metricsBuffer; return metricsBuffer;
} }
private ExecutionRecord toExecutionRecord(String agentId, String groupName, private ExecutionRecord toExecutionRecord(String agentId, String applicationName,
RouteExecution exec) { RouteExecution exec) {
String diagramHash = diagramStore String diagramHash = diagramStore
.findContentHashForRoute(exec.getRouteId(), agentId) .findContentHashForRoute(exec.getRouteId(), agentId)
.orElse(""); .orElse("");
return new ExecutionRecord( return new ExecutionRecord(
exec.getExchangeId(), exec.getRouteId(), agentId, groupName, exec.getExchangeId(), exec.getRouteId(), agentId, applicationName,
exec.getStatus() != null ? exec.getStatus().name() : "RUNNING", exec.getStatus() != null ? exec.getStatus().name() : "RUNNING",
exec.getCorrelationId(), exec.getExchangeId(), exec.getCorrelationId(), exec.getExchangeId(),
exec.getStartTime(), exec.getEndTime(), exec.getStartTime(), exec.getEndTime(),
@@ -90,13 +90,13 @@ public class IngestionService {
private List<ProcessorRecord> flattenProcessors( private List<ProcessorRecord> flattenProcessors(
List<ProcessorExecution> processors, String executionId, List<ProcessorExecution> processors, String executionId,
java.time.Instant execStartTime, String groupName, String routeId, java.time.Instant execStartTime, String applicationName, String routeId,
String parentProcessorId, int depth) { String parentProcessorId, int depth) {
List<ProcessorRecord> flat = new ArrayList<>(); List<ProcessorRecord> flat = new ArrayList<>();
for (ProcessorExecution p : processors) { for (ProcessorExecution p : processors) {
flat.add(new ProcessorRecord( flat.add(new ProcessorRecord(
executionId, p.getProcessorId(), p.getProcessorType(), executionId, p.getProcessorId(), p.getProcessorType(),
p.getDiagramNodeId(), groupName, routeId, p.getDiagramNodeId(), applicationName, routeId,
depth, parentProcessorId, depth, parentProcessorId,
p.getStatus() != null ? p.getStatus().name() : "RUNNING", p.getStatus() != null ? p.getStatus().name() : "RUNNING",
p.getStartTime() != null ? p.getStartTime() : execStartTime, p.getStartTime() != null ? p.getStartTime() : execStartTime,
@@ -109,7 +109,7 @@ public class IngestionService {
if (p.getChildren() != null) { if (p.getChildren() != null) {
flat.addAll(flattenProcessors( flat.addAll(flattenProcessors(
p.getChildren(), executionId, execStartTime, p.getChildren(), executionId, execStartTime,
groupName, routeId, p.getProcessorId(), depth + 1)); applicationName, routeId, p.getProcessorId(), depth + 1));
} }
} }
return flat; return flat;

View File

@@ -23,7 +23,7 @@ public record ExecutionSummary(
String executionId, String executionId,
String routeId, String routeId,
String agentId, String agentId,
String groupName, String applicationName,
String status, String status,
Instant startTime, Instant startTime,
Instant endTime, Instant endTime,

View File

@@ -22,7 +22,7 @@ import java.util.List;
* @param routeId exact match on route_id * @param routeId exact match on route_id
* @param agentId exact match on agent_id * @param agentId exact match on agent_id
* @param processorType matches processor_types array via has() * @param processorType matches processor_types array via has()
* @param group application group filter (resolved to agentIds server-side) * @param application application name filter (resolved to agentIds server-side)
* @param agentIds list of agent IDs (resolved from group, used for IN clause) * @param agentIds list of agent IDs (resolved from group, used for IN clause)
* @param offset pagination offset (0-based) * @param offset pagination offset (0-based)
* @param limit page size (default 50, max 500) * @param limit page size (default 50, max 500)
@@ -43,7 +43,7 @@ public record SearchRequest(
String routeId, String routeId,
String agentId, String agentId,
String processorType, String processorType,
String group, String application,
List<String> agentIds, List<String> agentIds,
int offset, int offset,
int limit, int limit,
@@ -80,12 +80,12 @@ public record SearchRequest(
return SORT_FIELD_TO_COLUMN.getOrDefault(sortField, "start_time"); return SORT_FIELD_TO_COLUMN.getOrDefault(sortField, "start_time");
} }
/** Create a copy with resolved agentIds (from group lookup). */ /** Create a copy with resolved agentIds (from application name lookup). */
public SearchRequest withAgentIds(List<String> resolvedAgentIds) { public SearchRequest withAgentIds(List<String> resolvedAgentIds) {
return new SearchRequest( return new SearchRequest(
status, timeFrom, timeTo, durationMin, durationMax, correlationId, status, timeFrom, timeTo, durationMin, durationMax, correlationId,
text, textInBody, textInHeaders, textInErrors, text, textInBody, textInHeaders, textInErrors,
routeId, agentId, processorType, group, resolvedAgentIds, routeId, agentId, processorType, application, resolvedAgentIds,
offset, limit, sortField, sortDir offset, limit, sortField, sortDir
); );
} }

View File

@@ -9,7 +9,7 @@ public interface ExecutionStore {
void upsert(ExecutionRecord execution); void upsert(ExecutionRecord execution);
void upsertProcessors(String executionId, Instant startTime, void upsertProcessors(String executionId, Instant startTime,
String groupName, String routeId, String applicationName, String routeId,
List<ProcessorRecord> processors); List<ProcessorRecord> processors);
Optional<ExecutionRecord> findById(String executionId); Optional<ExecutionRecord> findById(String executionId);
@@ -17,7 +17,7 @@ public interface ExecutionStore {
List<ProcessorRecord> findProcessors(String executionId); List<ProcessorRecord> findProcessors(String executionId);
record ExecutionRecord( record ExecutionRecord(
String executionId, String routeId, String agentId, String groupName, String executionId, String routeId, String agentId, String applicationName,
String status, String correlationId, String exchangeId, String status, String correlationId, String exchangeId,
Instant startTime, Instant endTime, Long durationMs, Instant startTime, Instant endTime, Long durationMs,
String errorMessage, String errorStacktrace, String diagramContentHash String errorMessage, String errorStacktrace, String diagramContentHash
@@ -25,7 +25,7 @@ public interface ExecutionStore {
record ProcessorRecord( record ProcessorRecord(
String executionId, String processorId, String processorType, String executionId, String processorId, String processorType,
String diagramNodeId, String groupName, String routeId, String diagramNodeId, String applicationName, String routeId,
int depth, String parentProcessorId, String status, int depth, String parentProcessorId, String status,
Instant startTime, Instant endTime, Long durationMs, Instant startTime, Instant endTime, Long durationMs,
String errorMessage, String errorStacktrace, String errorMessage, String errorStacktrace,

View File

@@ -12,7 +12,7 @@ public interface StatsStore {
ExecutionStats stats(Instant from, Instant to); ExecutionStats stats(Instant from, Instant to);
// Per-app stats (stats_1m_app) // Per-app stats (stats_1m_app)
ExecutionStats statsForApp(Instant from, Instant to, String groupName); ExecutionStats statsForApp(Instant from, Instant to, String applicationName);
// Per-route stats (stats_1m_route), optionally scoped to specific agents // Per-route stats (stats_1m_route), optionally scoped to specific agents
ExecutionStats statsForRoute(Instant from, Instant to, String routeId, List<String> agentIds); ExecutionStats statsForRoute(Instant from, Instant to, String routeId, List<String> agentIds);
@@ -24,7 +24,7 @@ public interface StatsStore {
StatsTimeseries timeseries(Instant from, Instant to, int bucketCount); StatsTimeseries timeseries(Instant from, Instant to, int bucketCount);
// Per-app timeseries // Per-app timeseries
StatsTimeseries timeseriesForApp(Instant from, Instant to, int bucketCount, String groupName); StatsTimeseries timeseriesForApp(Instant from, Instant to, int bucketCount, String applicationName);
// Per-route timeseries, optionally scoped to specific agents // Per-route timeseries, optionally scoped to specific agents
StatsTimeseries timeseriesForRoute(Instant from, Instant to, int bucketCount, StatsTimeseries timeseriesForRoute(Instant from, Instant to, int bucketCount,

View File

@@ -4,7 +4,7 @@ import java.time.Instant;
import java.util.List; import java.util.List;
public record ExecutionDocument( public record ExecutionDocument(
String executionId, String routeId, String agentId, String groupName, String executionId, String routeId, String agentId, String applicationName,
String status, String correlationId, String exchangeId, String status, String correlationId, String exchangeId,
Instant startTime, Instant endTime, Long durationMs, Instant startTime, Instant endTime, Long durationMs,
String errorMessage, String errorStacktrace, String errorMessage, String errorStacktrace,

591
dashboard-after-click.md Normal file
View File

@@ -0,0 +1,591 @@
- generic [ref=e3]:
- complementary [ref=e4]:
- generic [ref=e5] [cursor=pointer]:
- img [ref=e6]
- generic [ref=e7]: cameleerv3.2.1
- generic [ref=e9]:
- img [ref=e11]
- textbox "Filter..." [ref=e14]
- generic [ref=e15]:
- generic [ref=e16]: Navigation
- generic [ref=e17]:
- generic [ref=e18]:
- button "Collapse Applications" [expanded] [ref=e19] [cursor=pointer]: ▾
- button "Applications" [ref=e20] [cursor=pointer]
- tree [ref=e91]:
- treeitem "Collapse sample-app 214.3k Add to starred" [expanded] [ref=e686] [cursor=pointer]:
- button "Collapse" [ref=e94]: ▾
- generic [ref=e97]: sample-app
- generic [ref=e98]: 214.3k
- button "Add to starred" [ref=e99]:
- img [ref=e100]
- group [ref=e102]:
- treeitem "content-based-routing 0 Add to starred" [ref=e103] [cursor=pointer]:
- generic [ref=e105]: ▸
- generic [ref=e106]: content-based-routing
- generic [ref=e107]: "0"
- button "Add to starred" [ref=e108]:
- img [ref=e109]
- treeitem "data-gen-orders 8.6k Add to starred" [ref=e111] [cursor=pointer]:
- generic [ref=e113]: ▸
- generic [ref=e114]: data-gen-orders
- generic [ref=e115]: 8.6k
- button "Add to starred" [ref=e116]:
- img [ref=e117]
- treeitem "data-gen-files 6.2k Add to starred" [ref=e119] [cursor=pointer]:
- generic [ref=e121]: ▸
- generic [ref=e122]: data-gen-files
- generic [ref=e123]: 6.2k
- button "Add to starred" [ref=e124]:
- img [ref=e125]
- treeitem "data-gen-nested-split 4.8k Add to starred" [ref=e127] [cursor=pointer]:
- generic [ref=e129]: ▸
- generic [ref=e130]: data-gen-nested-split
- generic [ref=e131]: 4.8k
- button "Add to starred" [ref=e132]:
- img [ref=e133]
- treeitem "error-handling-test 5.8k Add to starred" [ref=e135] [cursor=pointer]:
- generic [ref=e137]: ▸
- generic [ref=e138]: error-handling-test
- generic [ref=e139]: 5.8k
- button "Add to starred" [ref=e140]:
- img [ref=e141]
- treeitem "file-processing 171.7k Add to starred" [ref=e687] [cursor=pointer]:
- generic [ref=e145]: ▸
- generic [ref=e146]: file-processing
- generic [ref=e147]: 171.7k
- button "Add to starred" [ref=e148]:
- img [ref=e149]
- treeitem "split-and-multicast 0 Add to starred" [ref=e151] [cursor=pointer]:
- generic [ref=e153]: ▸
- generic [ref=e154]: split-and-multicast
- generic [ref=e155]: "0"
- button "Add to starred" [ref=e156]:
- img [ref=e157]
- treeitem "notify-warehouse 0 Add to starred" [ref=e159] [cursor=pointer]:
- generic [ref=e161]: ▸
- generic [ref=e162]: notify-warehouse
- generic [ref=e163]: "0"
- button "Add to starred" [ref=e164]:
- img [ref=e165]
- treeitem "notify-billing 0 Add to starred" [ref=e167] [cursor=pointer]:
- generic [ref=e169]: ▸
- generic [ref=e170]: notify-billing
- generic [ref=e171]: "0"
- button "Add to starred" [ref=e172]:
- img [ref=e173]
- treeitem "nested-split-demo 0 Add to starred" [ref=e175] [cursor=pointer]:
- generic [ref=e177]: ▸
- generic [ref=e178]: nested-split-demo
- generic [ref=e179]: "0"
- button "Add to starred" [ref=e180]:
- img [ref=e181]
- treeitem "process-composite 0 Add to starred" [ref=e183] [cursor=pointer]:
- generic [ref=e185]: ▸
- generic [ref=e186]: process-composite
- generic [ref=e187]: "0"
- button "Add to starred" [ref=e188]:
- img [ref=e189]
- treeitem "process-order 0 Add to starred" [ref=e191] [cursor=pointer]:
- generic [ref=e193]: ▸
- generic [ref=e194]: process-order
- generic [ref=e195]: "0"
- button "Add to starred" [ref=e196]:
- img [ref=e197]
- treeitem "get-order 0 Add to starred" [ref=e199] [cursor=pointer]:
- generic [ref=e201]: ▸
- generic [ref=e202]: get-order
- generic [ref=e203]: "0"
- button "Add to starred" [ref=e204]:
- img [ref=e205]
- treeitem "route1 0 Add to starred" [ref=e207] [cursor=pointer]:
- generic [ref=e209]: ▸
- generic [ref=e210]: route1
- generic [ref=e211]: "0"
- button "Add to starred" [ref=e212]:
- img [ref=e213]
- treeitem "route2 0 Add to starred" [ref=e215] [cursor=pointer]:
- generic [ref=e217]: ▸
- generic [ref=e218]: route2
- generic [ref=e219]: "0"
- button "Add to starred" [ref=e220]:
- img [ref=e221]
- treeitem "timer-heartbeat 17.3k Add to starred" [ref=e223] [cursor=pointer]:
- generic [ref=e225]: ▸
- generic [ref=e226]: timer-heartbeat
- generic [ref=e227]: 17.3k
- button "Add to starred" [ref=e228]:
- img [ref=e229]
- generic [ref=e21]:
- generic [ref=e22]:
- button "Collapse Agents" [expanded] [ref=e23] [cursor=pointer]: ▾
- button "Agents" [ref=e24] [cursor=pointer]
- tree [ref=e231]:
- treeitem "Expand sample-app 1/1 live Add to starred" [ref=e232] [cursor=pointer]:
- button "Expand" [ref=e234]: ▸
- generic [ref=e237]: sample-app
- generic [ref=e238]: 1/1 live
- button "Add to starred" [ref=e239]:
- img [ref=e240]
- generic [ref=e25]:
- generic [ref=e26]:
- button "Collapse Routes" [expanded] [ref=e27] [cursor=pointer]: ▾
- button "Routes" [ref=e28] [cursor=pointer]
- tree [ref=e242]:
- treeitem "Expand sample-app 16 routes Add to starred" [ref=e243] [cursor=pointer]:
- button "Expand" [ref=e245]: ▸
- generic [ref=e248]: sample-app
- generic [ref=e249]: 16 routes
- button "Add to starred" [ref=e250]:
- img [ref=e251]
- generic [ref=e29]:
- button "⚙ Admin" [ref=e30] [cursor=pointer]:
- generic [ref=e31]: ⚙
- generic [ref=e33]: Admin
- button "☰ API Docs" [ref=e34] [cursor=pointer]:
- generic [ref=e35]: ☰
- generic [ref=e37]: API Docs
- generic [ref=e38]:
- banner [ref=e39]:
- navigation "Breadcrumb" [ref=e40]:
- list [ref=e41]:
- listitem [ref=e42]:
- generic [ref=e43]: apps
- button "Open search" [ref=e44] [cursor=pointer]:
- img [ref=e46]
- generic [ref=e49]: Search... ⌘K
- generic [ref=e50]: Ctrl+K
- group [ref=e51]:
- button "OK" [ref=e52] [cursor=pointer]: OK
- button "Warn" [ref=e54] [cursor=pointer]: Warn
- button "Error" [ref=e56] [cursor=pointer]: Error
- button "Running" [ref=e58] [cursor=pointer]: Running
- tablist [ref=e61]:
- tab "1h" [selected] [ref=e62] [cursor=pointer]:
- generic [ref=e63]: 1h
- tab "3h" [ref=e64] [cursor=pointer]:
- generic [ref=e65]: 3h
- tab "6h" [ref=e66] [cursor=pointer]:
- generic [ref=e67]: 6h
- tab "Today" [ref=e68] [cursor=pointer]:
- generic [ref=e69]: Today
- tab "24h" [ref=e70] [cursor=pointer]:
- generic [ref=e71]: 24h
- tab "7d" [ref=e72] [cursor=pointer]:
- generic [ref=e73]: 7d
- tab "23. März 19:40 now" [ref=e74]:
- generic [ref=e75]:
- button "23. März 19:40" [ref=e77] [cursor=pointer]
- generic [ref=e78]:
- button "now" [ref=e80] [cursor=pointer]
- generic [ref=e81]:
- button "Switch to dark mode" [ref=e82] [cursor=pointer]: ☾
- generic [ref=e85] [cursor=pointer]:
- generic [ref=e86]: admin
- generic "admin" [ref=e87]: AD
- main [ref=e88]:
- generic [ref=e253]:
- generic [ref=e254]:
- generic [ref=e255]:
- generic [ref=e256]:
- generic [ref=e257]: Exchanges
- img [ref=e258]
- generic [ref=e260]:
- generic [ref=e261]: 8,887
- generic [ref=e262]: ↓ -1%
- generic [ref=e263]: 100.0% success rate
- generic [ref=e264]:
- generic [ref=e266]: Success Rate
- generic [ref=e267]:
- generic [ref=e268]: 100.0%
- generic [ref=e269]: ↑ +0.0%
- generic [ref=e270]: 8,887 ok / 0 error
- generic [ref=e271]:
- generic [ref=e272]:
- generic [ref=e273]: Errors
- img [ref=e274]
- generic [ref=e275]:
- generic [ref=e276]: "0"
- generic [ref=e277]: → 0
- generic [ref=e278]: 0 errors in selected period
- generic [ref=e279]:
- generic [ref=e280]:
- generic [ref=e281]: Throughput
- img [ref=e282]
- generic [ref=e285]: "2.5"
- generic [ref=e286]: 2.5 msg/s
- generic [ref=e287]:
- generic [ref=e288]:
- generic [ref=e289]: Latency p99
- img [ref=e290]
- generic [ref=e293]: 8,804
- generic [ref=e294]: 8,804ms
- generic [ref=e295]:
- generic [ref=e296]:
- generic [ref=e297]: Recent Exchanges
- generic [ref=e298]:
- generic [ref=e299]: 50 of 8886 exchanges
- generic [ref=e300]: LIVE
- generic [ref=e301]:
- table [ref=e303]:
- rowgroup [ref=e304]:
- row "Status↕ ↕ Route↕ Application↕ Exchange ID↕ Started↕ Duration↕ Agent↕" [ref=e305]:
- columnheader "Status↕" [ref=e306] [cursor=pointer]
- columnheader "↕" [ref=e307] [cursor=pointer]
- columnheader "Route↕" [ref=e308] [cursor=pointer]
- columnheader "Application↕" [ref=e309] [cursor=pointer]
- columnheader "Exchange ID↕" [ref=e310] [cursor=pointer]
- columnheader "Started↕" [ref=e311] [cursor=pointer]
- columnheader "Duration↕" [ref=e312] [cursor=pointer]
- columnheader "Agent↕" [ref=e313] [cursor=pointer]
- rowgroup [ref=e314]:
- row "OK ↗ file-processing 6351ED788703FDC-0000000000109FA7 2026-03-23 19:40:45 783ms sample-app-1" [ref=e315] [cursor=pointer]:
- cell "OK" [ref=e316]:
- generic [ref=e319]: OK
- cell "↗" [ref=e320]:
- link "↗" [ref=e321]:
- /url: /exchanges/6351ED788703FDC-0000000000109FA7
- cell "file-processing" [ref=e322]
- cell [ref=e323]
- cell "6351ED788703FDC-0000000000109FA7" [ref=e324]
- cell "2026-03-23 19:40:45" [ref=e325]
- cell "783ms" [ref=e326]
- cell "sample-app-1" [ref=e327]:
- generic [ref=e328]: sample-app-1
- row "OK ↗ file-processing 6351ED788703FDC-0000000000109FA4 2026-03-23 19:40:45 865ms sample-app-1" [ref=e329] [cursor=pointer]:
- cell "OK" [ref=e330]:
- generic [ref=e333]: OK
- cell "↗" [ref=e334]:
- link "↗" [ref=e335]:
- /url: /exchanges/6351ED788703FDC-0000000000109FA4
- cell "file-processing" [ref=e336]
- cell [ref=e337]
- cell "6351ED788703FDC-0000000000109FA4" [ref=e338]
- cell "2026-03-23 19:40:45" [ref=e339]
- cell "865ms" [ref=e340]
- cell "sample-app-1" [ref=e341]:
- generic [ref=e342]: sample-app-1
- row "OK ↗ file-processing 6351ED788703FDC-0000000000109FA3 2026-03-23 19:40:44 415ms sample-app-1" [ref=e343] [cursor=pointer]:
- cell "OK" [ref=e344]:
- generic [ref=e347]: OK
- cell "↗" [ref=e348]:
- link "↗" [ref=e349]:
- /url: /exchanges/6351ED788703FDC-0000000000109FA3
- cell "file-processing" [ref=e350]
- cell [ref=e351]
- cell "6351ED788703FDC-0000000000109FA3" [ref=e352]
- cell "2026-03-23 19:40:44" [ref=e353]
- cell "415ms" [ref=e354]
- cell "sample-app-1" [ref=e355]:
- generic [ref=e356]: sample-app-1
- row "OK ↗ file-processing 6351ED788703FDC-0000000000109FA2 2026-03-23 19:40:44 120ms sample-app-1" [ref=e357] [cursor=pointer]:
- cell "OK" [ref=e358]:
- generic [ref=e361]: OK
- cell "↗" [ref=e362]:
- link "↗" [ref=e363]:
- /url: /exchanges/6351ED788703FDC-0000000000109FA2
- cell "file-processing" [ref=e364]
- cell [ref=e365]
- cell "6351ED788703FDC-0000000000109FA2" [ref=e366]
- cell "2026-03-23 19:40:44" [ref=e367]
- cell "120ms" [ref=e368]
- cell "sample-app-1" [ref=e369]:
- generic [ref=e370]: sample-app-1
- row "OK ↗ file-processing 6351ED788703FDC-0000000000109FA1 2026-03-23 19:40:44 194ms sample-app-1" [ref=e371] [cursor=pointer]:
- cell "OK" [ref=e372]:
- generic [ref=e375]: OK
- cell "↗" [ref=e376]:
- link "↗" [ref=e377]:
- /url: /exchanges/6351ED788703FDC-0000000000109FA1
- cell "file-processing" [ref=e378]
- cell [ref=e379]
- cell "6351ED788703FDC-0000000000109FA1" [ref=e380]
- cell "2026-03-23 19:40:44" [ref=e381]
- cell "194ms" [ref=e382]
- cell "sample-app-1" [ref=e383]:
- generic [ref=e384]: sample-app-1
- row "OK ↗ timer-heartbeat 6351ED788703FDC-0000000000109FA0 2026-03-23 19:40:43 840ms sample-app-1" [ref=e385] [cursor=pointer]:
- cell "OK" [ref=e386]:
- generic [ref=e389]: OK
- cell "↗" [ref=e390]:
- link "↗" [ref=e391]:
- /url: /exchanges/6351ED788703FDC-0000000000109FA0
- cell "timer-heartbeat" [ref=e392]
- cell [ref=e393]
- cell "6351ED788703FDC-0000000000109FA0" [ref=e394]
- cell "2026-03-23 19:40:43" [ref=e395]
- cell "840ms" [ref=e396]
- cell "sample-app-1" [ref=e397]:
- generic [ref=e398]: sample-app-1
- row "OK ↗ file-processing 6351ED788703FDC-0000000000109F9F 2026-03-23 19:40:43 476ms sample-app-1" [ref=e399] [cursor=pointer]:
- cell "OK" [ref=e400]:
- generic [ref=e403]: OK
- cell "↗" [ref=e404]:
- link "↗" [ref=e405]:
- /url: /exchanges/6351ED788703FDC-0000000000109F9F
- cell "file-processing" [ref=e406]
- cell [ref=e407]
- cell "6351ED788703FDC-0000000000109F9F" [ref=e408]
- cell "2026-03-23 19:40:43" [ref=e409]
- cell "476ms" [ref=e410]
- cell "sample-app-1" [ref=e411]:
- generic [ref=e412]: sample-app-1
- row "OK ↗ data-gen-orders 6351ED788703FDC-0000000000109F9E 2026-03-23 19:40:43 1.9s sample-app-1" [ref=e413] [cursor=pointer]:
- cell "OK" [ref=e414]:
- generic [ref=e417]: OK
- cell "↗" [ref=e418]:
- link "↗" [ref=e419]:
- /url: /exchanges/6351ED788703FDC-0000000000109F9E
- cell "data-gen-orders" [ref=e420]
- cell [ref=e421]
- cell "6351ED788703FDC-0000000000109F9E" [ref=e422]
- cell "2026-03-23 19:40:43" [ref=e423]
- cell "1.9s" [ref=e424]
- cell "sample-app-1" [ref=e425]:
- generic [ref=e426]: sample-app-1
- row "OK ↗ file-processing 6351ED788703FDC-0000000000109F9D 2026-03-23 19:40:43 443ms sample-app-1" [ref=e427] [cursor=pointer]:
- cell "OK" [ref=e428]:
- generic [ref=e431]: OK
- cell "↗" [ref=e432]:
- link "↗" [ref=e433]:
- /url: /exchanges/6351ED788703FDC-0000000000109F9D
- cell "file-processing" [ref=e434]
- cell [ref=e435]
- cell "6351ED788703FDC-0000000000109F9D" [ref=e436]
- cell "2026-03-23 19:40:43" [ref=e437]
- cell "443ms" [ref=e438]
- cell "sample-app-1" [ref=e439]:
- generic [ref=e440]: sample-app-1
- row "OK ↗ file-processing 6351ED788703FDC-0000000000109F9C 2026-03-23 19:40:43 179ms sample-app-1" [ref=e441] [cursor=pointer]:
- cell "OK" [ref=e442]:
- generic [ref=e445]: OK
- cell "↗" [ref=e446]:
- link "↗" [ref=e447]:
- /url: /exchanges/6351ED788703FDC-0000000000109F9C
- cell "file-processing" [ref=e448]
- cell [ref=e449]
- cell "6351ED788703FDC-0000000000109F9C" [ref=e450]
- cell "2026-03-23 19:40:43" [ref=e451]
- cell "179ms" [ref=e452]
- cell "sample-app-1" [ref=e453]:
- generic [ref=e454]: sample-app-1
- row "OK ↗ file-processing 6351ED788703FDC-0000000000109F9B 2026-03-23 19:40:42 240ms sample-app-1" [ref=e455] [cursor=pointer]:
- cell "OK" [ref=e456]:
- generic [ref=e459]: OK
- cell "↗" [ref=e460]:
- link "↗" [ref=e461]:
- /url: /exchanges/6351ED788703FDC-0000000000109F9B
- cell "file-processing" [ref=e462]
- cell [ref=e463]
- cell "6351ED788703FDC-0000000000109F9B" [ref=e464]
- cell "2026-03-23 19:40:42" [ref=e465]
- cell "240ms" [ref=e466]
- cell "sample-app-1" [ref=e467]:
- generic [ref=e468]: sample-app-1
- row "OK ↗ file-processing 6351ED788703FDC-0000000000109F9A 2026-03-23 19:40:42 798ms sample-app-1" [ref=e469] [cursor=pointer]:
- cell "OK" [ref=e470]:
- generic [ref=e473]: OK
- cell "↗" [ref=e474]:
- link "↗" [ref=e475]:
- /url: /exchanges/6351ED788703FDC-0000000000109F9A
- cell "file-processing" [ref=e476]
- cell [ref=e477]
- cell "6351ED788703FDC-0000000000109F9A" [ref=e478]
- cell "2026-03-23 19:40:42" [ref=e479]
- cell "798ms" [ref=e480]
- cell "sample-app-1" [ref=e481]:
- generic [ref=e482]: sample-app-1
- row "OK ↗ file-processing 6351ED788703FDC-0000000000109F98 2026-03-23 19:40:41 276ms sample-app-1" [ref=e483] [cursor=pointer]:
- cell "OK" [ref=e484]:
- generic [ref=e487]: OK
- cell "↗" [ref=e488]:
- link "↗" [ref=e489]:
- /url: /exchanges/6351ED788703FDC-0000000000109F98
- cell "file-processing" [ref=e490]
- cell [ref=e491]
- cell "6351ED788703FDC-0000000000109F98" [ref=e492]
- cell "2026-03-23 19:40:41" [ref=e493]
- cell "276ms" [ref=e494]
- cell "sample-app-1" [ref=e495]:
- generic [ref=e496]: sample-app-1
- row "OK ↗ file-processing 6351ED788703FDC-0000000000109F96 2026-03-23 19:40:41 213ms sample-app-1" [ref=e497] [cursor=pointer]:
- cell "OK" [ref=e498]:
- generic [ref=e501]: OK
- cell "↗" [ref=e502]:
- link "↗" [ref=e503]:
- /url: /exchanges/6351ED788703FDC-0000000000109F96
- cell "file-processing" [ref=e504]
- cell [ref=e505]
- cell "6351ED788703FDC-0000000000109F96" [ref=e506]
- cell "2026-03-23 19:40:41" [ref=e507]
- cell "213ms" [ref=e508]
- cell "sample-app-1" [ref=e509]:
- generic [ref=e510]: sample-app-1
- row "OK ↗ file-processing 6351ED788703FDC-0000000000109F95 2026-03-23 19:40:41 15ms sample-app-1" [ref=e511] [cursor=pointer]:
- cell "OK" [ref=e512]:
- generic [ref=e515]: OK
- cell "↗" [ref=e516]:
- link "↗" [ref=e517]:
- /url: /exchanges/6351ED788703FDC-0000000000109F95
- cell "file-processing" [ref=e518]
- cell [ref=e519]
- cell "6351ED788703FDC-0000000000109F95" [ref=e520]
- cell "2026-03-23 19:40:41" [ref=e521]
- cell "15ms" [ref=e522]
- cell "sample-app-1" [ref=e523]:
- generic [ref=e524]: sample-app-1
- row "OK ↗ file-processing 6351ED788703FDC-0000000000109F94 2026-03-23 19:40:41 537ms sample-app-1" [ref=e525] [cursor=pointer]:
- cell "OK" [ref=e526]:
- generic [ref=e529]: OK
- cell "↗" [ref=e530]:
- link "↗" [ref=e531]:
- /url: /exchanges/6351ED788703FDC-0000000000109F94
- cell "file-processing" [ref=e532]
- cell [ref=e533]
- cell "6351ED788703FDC-0000000000109F94" [ref=e534]
- cell "2026-03-23 19:40:41" [ref=e535]
- cell "537ms" [ref=e536]
- cell "sample-app-1" [ref=e537]:
- generic [ref=e538]: sample-app-1
- row "OK ↗ file-processing 6351ED788703FDC-0000000000109F93 2026-03-23 19:40:40 244ms sample-app-1" [ref=e539] [cursor=pointer]:
- cell "OK" [ref=e540]:
- generic [ref=e543]: OK
- cell "↗" [ref=e544]:
- link "↗" [ref=e545]:
- /url: /exchanges/6351ED788703FDC-0000000000109F93
- cell "file-processing" [ref=e546]
- cell [ref=e547]
- cell "6351ED788703FDC-0000000000109F93" [ref=e548]
- cell "2026-03-23 19:40:40" [ref=e549]
- cell "244ms" [ref=e550]
- cell "sample-app-1" [ref=e551]:
- generic [ref=e552]: sample-app-1
- row "OK ↗ file-processing 6351ED788703FDC-0000000000109F90 2026-03-23 19:40:39 867ms sample-app-1" [ref=e553] [cursor=pointer]:
- cell "OK" [ref=e554]:
- generic [ref=e557]: OK
- cell "↗" [ref=e558]:
- link "↗" [ref=e559]:
- /url: /exchanges/6351ED788703FDC-0000000000109F90
- cell "file-processing" [ref=e560]
- cell [ref=e561]
- cell "6351ED788703FDC-0000000000109F90" [ref=e562]
- cell "2026-03-23 19:40:39" [ref=e563]
- cell "867ms" [ref=e564]
- cell "sample-app-1" [ref=e565]:
- generic [ref=e566]: sample-app-1
- row "OK ↗ file-processing 6351ED788703FDC-0000000000109F8F 2026-03-23 19:40:39 389ms sample-app-1" [ref=e567] [cursor=pointer]:
- cell "OK" [ref=e568]:
- generic [ref=e571]: OK
- cell "↗" [ref=e572]:
- link "↗" [ref=e573]:
- /url: /exchanges/6351ED788703FDC-0000000000109F8F
- cell "file-processing" [ref=e574]
- cell [ref=e575]
- cell "6351ED788703FDC-0000000000109F8F" [ref=e576]
- cell "2026-03-23 19:40:39" [ref=e577]
- cell "389ms" [ref=e578]
- cell "sample-app-1" [ref=e579]:
- generic [ref=e580]: sample-app-1
- row "OK ↗ file-processing 6351ED788703FDC-0000000000109F8D 2026-03-23 19:40:39 25ms sample-app-1" [ref=e581] [cursor=pointer]:
- cell "OK" [ref=e582]:
- generic [ref=e585]: OK
- cell "↗" [ref=e586]:
- link "↗" [ref=e587]:
- /url: /exchanges/6351ED788703FDC-0000000000109F8D
- cell "file-processing" [ref=e588]
- cell [ref=e589]
- cell "6351ED788703FDC-0000000000109F8D" [ref=e590]
- cell "2026-03-23 19:40:39" [ref=e591]
- cell "25ms" [ref=e592]
- cell "sample-app-1" [ref=e593]:
- generic [ref=e594]: sample-app-1
- row "OK ↗ timer-heartbeat 6351ED788703FDC-0000000000109F8C 2026-03-23 19:40:38 679ms sample-app-1" [ref=e595] [cursor=pointer]:
- cell "OK" [ref=e596]:
- generic [ref=e599]: OK
- cell "↗" [ref=e600]:
- link "↗" [ref=e601]:
- /url: /exchanges/6351ED788703FDC-0000000000109F8C
- cell "timer-heartbeat" [ref=e602]
- cell [ref=e603]
- cell "6351ED788703FDC-0000000000109F8C" [ref=e604]
- cell "2026-03-23 19:40:38" [ref=e605]
- cell "679ms" [ref=e606]
- cell "sample-app-1" [ref=e607]:
- generic [ref=e608]: sample-app-1
- row "OK ↗ file-processing 6351ED788703FDC-0000000000109F8A 2026-03-23 19:40:38 922ms sample-app-1" [ref=e609] [cursor=pointer]:
- cell "OK" [ref=e610]:
- generic [ref=e613]: OK
- cell "↗" [ref=e614]:
- link "↗" [ref=e615]:
- /url: /exchanges/6351ED788703FDC-0000000000109F8A
- cell "file-processing" [ref=e616]
- cell [ref=e617]
- cell "6351ED788703FDC-0000000000109F8A" [ref=e618]
- cell "2026-03-23 19:40:38" [ref=e619]
- cell "922ms" [ref=e620]
- cell "sample-app-1" [ref=e621]:
- generic [ref=e622]: sample-app-1
- row "OK ↗ file-processing 6351ED788703FDC-0000000000109F88 2026-03-23 19:40:37 851ms sample-app-1" [ref=e623] [cursor=pointer]:
- cell "OK" [ref=e624]:
- generic [ref=e627]: OK
- cell "↗" [ref=e628]:
- link "↗" [ref=e629]:
- /url: /exchanges/6351ED788703FDC-0000000000109F88
- cell "file-processing" [ref=e630]
- cell [ref=e631]
- cell "6351ED788703FDC-0000000000109F88" [ref=e632]
- cell "2026-03-23 19:40:37" [ref=e633]
- cell "851ms" [ref=e634]
- cell "sample-app-1" [ref=e635]:
- generic [ref=e636]: sample-app-1
- row "OK ↗ file-processing 6351ED788703FDC-0000000000109F87 2026-03-23 19:40:37 10ms sample-app-1" [ref=e637] [cursor=pointer]:
- cell "OK" [ref=e638]:
- generic [ref=e641]: OK
- cell "↗" [ref=e642]:
- link "↗" [ref=e643]:
- /url: /exchanges/6351ED788703FDC-0000000000109F87
- cell "file-processing" [ref=e644]
- cell [ref=e645]
- cell "6351ED788703FDC-0000000000109F87" [ref=e646]
- cell "2026-03-23 19:40:37" [ref=e647]
- cell "10ms" [ref=e648]
- cell "sample-app-1" [ref=e649]:
- generic [ref=e650]: sample-app-1
- row "OK ↗ data-gen-files 6351ED788703FDC-0000000000109F84 2026-03-23 19:40:37 0ms sample-app-1" [ref=e651] [cursor=pointer]:
- cell "OK" [ref=e652]:
- generic [ref=e655]: OK
- cell "↗" [ref=e656]:
- link "↗" [ref=e657]:
- /url: /exchanges/6351ED788703FDC-0000000000109F84
- cell "data-gen-files" [ref=e658]
- cell [ref=e659]
- cell "6351ED788703FDC-0000000000109F84" [ref=e660]
- cell "2026-03-23 19:40:37" [ref=e661]
- cell "0ms" [ref=e662]
- cell "sample-app-1" [ref=e663]:
- generic [ref=e664]: sample-app-1
- generic [ref=e665]:
- generic [ref=e666]: 125 of 50
- generic [ref=e667]:
- generic [ref=e668]: "Rows:"
- generic [ref=e669]:
- combobox [ref=e670] [cursor=pointer]:
- option "10"
- option "25" [selected]
- option "50"
- option "100"
- generic: ▾
- generic [ref=e671]:
- button "Previous page" [disabled] [ref=e672]:
- generic [ref=e673]:
- generic [ref=e674]: 1 / 2
- button "Next page" [ref=e675] [cursor=pointer]:
- generic [ref=e676]:
- complementary [ref=e677]:
- generic [ref=e678]:
- generic [ref=e679]: Exchange 6351ED788703...
- button "Close panel" [ref=e680] [cursor=pointer]: ×
- generic [ref=e681]:
- button "Overview" [ref=e682] [cursor=pointer]
- button "Processors" [ref=e683] [cursor=pointer]
- button "Route Flow" [ref=e684] [cursor=pointer]

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,142 @@
# RBAC CRUD Gaps — Design Specification
## Goal
Add missing CRUD and assignment UI to the RBAC management page, fix date formatting, seed a built-in Admins group, and fix dashboard diagram ordering.
## References
- Parent spec: `docs/superpowers/specs/2026-03-17-rbac-management-design.md`
- Visual prototype: `examples/RBAC/rbac_management_ui.html`
---
## Changes
### 1. Users Tab — Delete + Assignments
Users cannot be created manually (they arrive via login). The detail pane gains:
- **Delete button** in the detail header area. Uses existing `ConfirmDeleteDialog` with the user's `displayName` as the confirmation string. Calls `useDeleteUser()`. **Guard:** the currently authenticated user (from `useAuthStore`) cannot delete themselves — button disabled with tooltip "Cannot delete your own account".
- **Group membership section** — "+ Add" chip opens a **multi-select dropdown** listing all groups the user is NOT already a member of. Checkboxes for batch selection, "Apply" button to commit. Calls are batched via `Promise.allSettled()` — if any fail, show an inline error, invalidate queries regardless to refresh. Existing group chips gain an "x" remove button calling `useRemoveUserFromGroup()`.
- **Direct roles section** — the existing "Effective roles" section renders both direct and inherited roles. The "+ Add" multi-select dropdown lists roles not yet directly assigned. Calls `useAssignRoleToUser()` (batched via `Promise.allSettled()`). Direct role chips gain an "x" button calling `useRemoveRoleFromUser()`. Inherited role chips (dashed border) do NOT get remove buttons — they can only be removed by changing group membership or group role assignments.
- **Created field** — change from date-only to full date+time: `new Date(createdAt).toLocaleString()`.
- **Mutation button states** — all action buttons (delete, remove chip "x") disable while their mutation is in-flight to prevent double-clicks.
### 2. Groups Tab — CRUD + Assignments
- **"+ Add group" button** in the panel header (`.btnAdd` style exists). Opens an inline form below the search bar with: name text input, optional parent group dropdown, "Create" button. Calls `useCreateGroup()`. Form clears and closes on success. On error: shows error message inline.
- **Delete button** in detail pane header. Uses `ConfirmDeleteDialog` with group name. Calls `useDeleteGroup()`. Resets selected group. **Guard:** the built-in Admins group (`SystemRole.ADMINS_GROUP_ID`) cannot be deleted — button disabled with tooltip "Built-in group cannot be deleted".
- **Assigned roles section** — "+ Add" multi-select dropdown listing roles not yet assigned to this group. Batched via `Promise.allSettled()`. Calls `useAssignRoleToGroup()`. Role chips gain "x" for `useRemoveRoleFromGroup()`.
- **Parent group** — shown as a dropdown in the detail header area, allowing re-parenting. Calls `useUpdateGroup()`. The dropdown excludes the group itself and its transitive descendants (cycle prevention — requires recursive traversal of `childGroups` on each `GroupDetail`). Setting to empty/none makes it top-level.
### 3. Roles Tab — CRUD
- **"+ Add role" button** in panel header. Opens an inline form: name (required), description (optional), scope (optional, free-text, defaults to "custom"). Calls `useCreateRole()`.
- **Delete button** in detail pane header. **Disabled for system roles** (lock icon + tooltip "System roles cannot be deleted"). Custom roles use `ConfirmDeleteDialog` with role name → `useDeleteRole()`.
- No assignment UI on the roles tab — assignments are managed from the User and Group detail panes.
### 4. Multi-Select Dropdown Component
A reusable component used across all assignment actions:
```
Props:
items: { id: string; label: string }[] — available items to pick from
onApply: (selectedIds: string[]) => void — called with all checked IDs
placeholder?: string — search filter placeholder
```
Behavior:
- Opens as a positioned dropdown below the "+ Add" chip
- Search/filter input at top
- Checkbox list of items (max-height with scroll)
- "Apply" button at bottom (disabled when nothing selected)
- Closes on Apply, Escape, or click-outside
- Shows count badge on Apply button: "Apply (3)"
Styling: background `var(--bg-raised)`, border `var(--border)`, border-radius `var(--radius-md)`, items with `var(--bg-hover)` on hover, checkboxes with `var(--amber)` accent.
### 5. Inline Create Form
A reusable pattern for "Add group" and "Add role":
- Appears below the search bar in the list pane, pushing content down
- Input fields with labels
- "Create" and "Cancel" buttons
- On success: closes form, clears inputs, new entity appears in list
- On error: shows error message inline
- "Create" button disabled while mutation is in-flight
### 6. Built-in Admins Group Seed
**Database migration** — new `V2__admin_group_seed.sql` (V1 is already deployed, V2-V10 were deleted in the migration consolidation so V2 is safe):
```sql
-- Built-in Admins group
INSERT INTO groups (id, name) VALUES
('00000000-0000-0000-0000-000000000010', 'Admins');
-- Assign ADMIN role to Admins group
INSERT INTO group_roles (group_id, role_id) VALUES
('00000000-0000-0000-0000-000000000010', '00000000-0000-0000-0000-000000000004');
```
**SystemRole.java** — add constants:
```java
public static final UUID ADMINS_GROUP_ID = UUID.fromString("00000000-0000-0000-0000-000000000010");
```
**UiAuthController.login()** — after upserting the user and assigning ADMIN role, also add to Admins group:
```java
rbacService.addUserToGroup(subject, SystemRole.ADMINS_GROUP_ID);
```
**Frontend guard:** The Admins group UUID is hardcoded as a constant in the frontend to disable deletion. Alternatively, check if a group's ID matches a known system group ID.
### 7. Dashboard Diagram Ordering
The inheritance diagram's three columns (Groups → Roles → Users) must show items in a consistent, matching order:
- **Groups column**: alphabetical by name, children indented under parents
- **Roles column**: iterate groups top-to-bottom, collect their direct roles, deduplicate preserving first-seen order. Roles not assigned to any group are omitted from the diagram.
- **Users column**: alphabetical by display name
Sort explicitly in `DashboardTab.tsx` before rendering.
---
## Files Changed
### Frontend — Modified
| File | Change |
|---|---|
| `ui/src/pages/admin/rbac/UsersTab.tsx` | Delete button, group/role assignment dropdowns, date format fix, self-delete guard |
| `ui/src/pages/admin/rbac/GroupsTab.tsx` | Add group form, delete button, role assignment dropdown, parent group dropdown, Admins guard |
| `ui/src/pages/admin/rbac/RolesTab.tsx` | Add role form, delete button (disabled for system) |
| `ui/src/pages/admin/rbac/DashboardTab.tsx` | Sort diagram columns consistently |
| `ui/src/pages/admin/rbac/RbacPage.module.css` | Styles for multi-select dropdown, inline create form, delete button, action chips, remove buttons |
### Frontend — New
| File | Responsibility |
|---|---|
| `ui/src/pages/admin/rbac/components/MultiSelectDropdown.tsx` | Reusable multi-select picker with search, checkboxes, batch apply |
### Backend — Modified
| File | Change |
|---|---|
| `cameleer3-server-core/.../rbac/SystemRole.java` | Add `ADMINS_GROUP_ID` constant |
| `cameleer3-server-app/.../security/UiAuthController.java` | Add admin user to Admins group on login |
### Backend — New Migration
| File | Change |
|---|---|
| `cameleer3-server-app/src/main/resources/db/migration/V2__admin_group_seed.sql` | Seed Admins group + ADMIN role assignment |
---
## Out of Scope
- Editing user profile fields (name, email) — users are managed by their identity provider
- Drag-and-drop group hierarchy management
- Role permission editing (custom roles have no effect on Spring Security yet)

View File

@@ -0,0 +1,327 @@
# RBAC Management — Design Specification
## Goal
Implement a full RBAC management system (issue #41) with group hierarchy, role inheritance, and a management UI integrated into the admin section. Replace the flat `users.roles` text array with a proper relational model.
## References
- Functional spec: `examples/RBAC/rbac-ui-spec.md`
- Visual prototype: `examples/RBAC/rbac_management_ui.html`
---
## Backend
### Database Schema
Squash V1V10 Flyway migrations into a single `V1__init.sql`. The `users` table drops the `roles TEXT[]` column. New tables:
```sql
CREATE TABLE users (
user_id TEXT PRIMARY KEY,
provider TEXT NOT NULL,
email TEXT,
display_name TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
-- RBAC: all roles — system roles seeded with fixed UUIDs, custom roles created by admins
CREATE TABLE roles (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name TEXT NOT NULL UNIQUE,
description TEXT NOT NULL DEFAULT '',
scope TEXT NOT NULL DEFAULT 'custom',
system BOOLEAN NOT NULL DEFAULT false,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
-- Seed system roles with fixed UUIDs (stable across environments)
INSERT INTO roles (id, name, description, scope, system) VALUES
('00000000-0000-0000-0000-000000000001', 'AGENT', 'Agent registration and data ingestion', 'system-wide', true),
('00000000-0000-0000-0000-000000000002', 'VIEWER', 'Read-only access to dashboards and data', 'system-wide', true),
('00000000-0000-0000-0000-000000000003', 'OPERATOR', 'Operational commands (start/stop/configure agents)', 'system-wide', true),
('00000000-0000-0000-0000-000000000004', 'ADMIN', 'Full administrative access', 'system-wide', true);
-- RBAC: groups with self-referential hierarchy
CREATE TABLE groups (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name TEXT NOT NULL UNIQUE,
parent_group_id UUID REFERENCES groups(id) ON DELETE SET NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
-- Join: roles assigned to groups (system + custom)
CREATE TABLE group_roles (
group_id UUID NOT NULL REFERENCES groups(id) ON DELETE CASCADE,
role_id UUID NOT NULL REFERENCES roles(id) ON DELETE CASCADE,
PRIMARY KEY (group_id, role_id)
);
-- Join: direct group membership for users
CREATE TABLE user_groups (
user_id TEXT NOT NULL REFERENCES users(user_id) ON DELETE CASCADE,
group_id UUID NOT NULL REFERENCES groups(id) ON DELETE CASCADE,
PRIMARY KEY (user_id, group_id)
);
-- Join: direct role assignments to users (system + custom)
CREATE TABLE user_roles (
user_id TEXT NOT NULL REFERENCES users(user_id) ON DELETE CASCADE,
role_id UUID NOT NULL REFERENCES roles(id) ON DELETE CASCADE,
PRIMARY KEY (user_id, role_id)
);
-- Indexes for join query performance
CREATE INDEX idx_user_roles_user_id ON user_roles(user_id);
CREATE INDEX idx_user_groups_user_id ON user_groups(user_id);
CREATE INDEX idx_group_roles_group_id ON group_roles(group_id);
CREATE INDEX idx_groups_parent ON groups(parent_group_id);
```
Note: `roles TEXT[]` column is removed from `users`. All roles (system and custom) live in the `roles` table. System roles are seeded rows with `system = true` and fixed UUIDs — the application prevents their deletion or modification.
### System Roles
The four system roles (AGENT, VIEWER, OPERATOR, ADMIN) are:
- Seeded as rows in the `roles` table with `system = true` and fixed UUIDs
- Assigned to users via the same `user_roles` join table as custom roles
- Protected by application logic: creation, deletion, and name/scope modification are rejected
- Displayed in the UI as read-only entries (lock icon, non-deletable)
- Used by Spring Security / JWT for authorization decisions
Custom roles (`system = false`) are application-defined and have no effect on Spring Security — they serve the RBAC management model only (for future permission expansion).
The `scope` field distinguishes role domains: system roles use `system-wide`, custom roles can use descriptive scopes like `monitoring:read`, `config:write` for future permission gating.
### Domain Model (Java)
```java
// Existing, modified — drop roles field
public record UserInfo(
String userId,
String provider,
String email,
String displayName,
Instant createdAt
) {}
// New — enriched user for admin API responses
public record UserDetail(
String userId,
String provider,
String email,
String displayName,
Instant createdAt,
List<RoleSummary> directRoles, // from user_roles join (system + custom)
List<GroupSummary> directGroups, // from user_groups join
List<RoleSummary> effectiveRoles, // computed: union of direct + inherited via groups
List<GroupSummary> effectiveGroups // computed: direct groups + their ancestor chain
) {}
public record GroupDetail(
UUID id,
String name,
UUID parentGroupId, // nullable
Instant createdAt,
List<RoleSummary> directRoles,
List<RoleSummary> effectiveRoles, // direct + inherited from parent chain
List<UserSummary> members, // direct members
List<GroupSummary> childGroups
) {}
public record RoleDetail(
UUID id,
String name,
String description,
String scope,
boolean system, // true for AGENT/VIEWER/OPERATOR/ADMIN
Instant createdAt,
List<GroupSummary> assignedGroups,
List<UserSummary> directUsers,
List<UserSummary> effectivePrincipals // all users who hold this role
) {}
// Summaries for embedding in detail responses
public record UserSummary(String userId, String displayName, String provider) {}
public record GroupSummary(UUID id, String name) {}
public record RoleSummary(UUID id, String name, boolean system, String source) {}
// source: "direct" | group name (for inherited)
```
### Inheritance Logic
Server-side computation in a service class (e.g., `RbacService`):
1. **Effective groups for user**: Start from `user_groups` (direct memberships), then for each group walk `parent_group_id` chain upward to collect all ancestor groups. The union is every group the user is transitively a member of.
2. **Effective roles for user**: Direct `user_roles` + all `group_roles` for every effective group. Both system and custom roles flow through the same path.
3. **Effective roles for group**: Direct `group_roles` + inherited from parent chain.
4. **Effective principals for role**: All users who hold the role directly + all users in any group that has the role (transitively).
No role negation — roles only grant, never deny.
**Cycle detection**: When setting `parent_group_id` on a group, the application must walk the proposed parent chain upward and reject the update if it would create a cycle (i.e., the group appears in its own ancestor chain). Return HTTP 409 Conflict.
### Auth Integration
`JwtService` and `SecurityConfig` read system roles from `user_roles` joined to `roles WHERE system = true`, instead of `users.roles`. The `UserRepository` methods that currently read/write `users.roles` are updated to use the join table. JWT claims remain unchanged (`roles: ["ADMIN", "VIEWER"]`).
OIDC auto-signup: When a user is auto-registered via OIDC token exchange, they get a row in `users` with `provider = "oidc:<issuer>"` and a default system role (VIEWER) via `user_roles`. No group membership by default.
### API Endpoints
All under `/api/v1/admin/` prefix, protected by `@PreAuthorize("hasRole('ADMIN')")`.
The existing `PUT /users/{userId}/roles` bulk endpoint is removed. Role assignments use individual add/remove endpoints.
All mutation endpoints log to the `AuditService` (category: `USER_MGMT` for user operations, `RBAC` for group/role operations).
**Users** — response type: `UserDetail`
| Method | Path | Description | Request Body |
|---|---|---|---|
| GET | `/users` | List all users with effective roles/groups | — |
| GET | `/users/{id}` | Full user detail | — |
| POST | `/users/{id}/roles/{roleId}` | Assign role to user (system or custom) | — |
| DELETE | `/users/{id}/roles/{roleId}` | Remove role from user | — |
| POST | `/users/{id}/groups/{groupId}` | Add user to group | — |
| DELETE | `/users/{id}/groups/{groupId}` | Remove user from group | — |
| DELETE | `/users/{id}` | Delete user | — |
**Groups** — response type: `GroupDetail`
| Method | Path | Description | Request Body |
|---|---|---|---|
| GET | `/groups` | List all groups with hierarchy | — |
| GET | `/groups/{id}` | Full group detail | — |
| POST | `/groups` | Create group | `{ name, parentGroupId? }` |
| PUT | `/groups/{id}` | Update group | `{ name?, parentGroupId? }` — returns 409 on cycle |
| DELETE | `/groups/{id}` | Delete group — cascades role/member associations; child groups become top-level (parent set to null) | — |
| POST | `/groups/{id}/roles/{roleId}` | Assign role to group | — |
| DELETE | `/groups/{id}/roles/{roleId}` | Remove role from group | — |
**Roles** — response type: `RoleDetail`
| Method | Path | Description | Request Body |
|---|---|---|---|
| GET | `/roles` | List all roles (system + custom) | — |
| GET | `/roles/{id}` | Role detail | — |
| POST | `/roles` | Create custom role | `{ name, description?, scope? }` |
| PUT | `/roles/{id}` | Update custom role (rejects system roles) | `{ name?, description?, scope? }` |
| DELETE | `/roles/{id}` | Delete custom role (rejects system roles) | — |
**Dashboard:**
| Method | Path | Description |
|---|---|---|
| GET | `/rbac/stats` | `{ userCount, activeUserCount, groupCount, maxGroupDepth, roleCount }` |
---
## Frontend
### Routing
New route at `/admin/rbac` in `router.tsx`, lazy-loaded:
```tsx
const RbacPage = lazy(() => import('./pages/admin/rbac/RbacPage').then(m => ({ default: m.RbacPage })));
// ...
{ path: 'admin/rbac', element: <Suspense fallback={null}><RbacPage /></Suspense> }
```
Update `AppSidebar` ADMIN_LINKS to add `{ to: '/admin/rbac', label: 'User Management' }`.
### Component Structure
```
pages/admin/rbac/
├── RbacPage.tsx ← ADMIN role gate + tab navigation
├── RbacPage.module.css ← All RBAC-specific styles
├── DashboardTab.tsx ← Stat cards + inheritance diagram
├── UsersTab.tsx ← Split pane orchestrator
├── GroupsTab.tsx ← Split pane orchestrator
├── RolesTab.tsx ← Split pane orchestrator
├── components/
│ ├── EntityListPane.tsx ← Reusable: search input + scrollable card list
│ ├── EntityCard.tsx ← Single list row: avatar, name, meta, tags, status dot
│ ├── UserDetail.tsx ← Header, fields, groups, effective roles, group tree
│ ├── GroupDetail.tsx ← Header, fields, members, children, roles, hierarchy
│ ├── RoleDetail.tsx ← Header, fields, assigned groups/users, effective principals
│ ├── InheritanceChip.tsx ← Chip with dashed border + "↑ Source" annotation
│ ├── GroupTree.tsx ← Indented tree with corner connectors
│ ├── EntityAvatar.tsx ← Circle (user), rounded-square (group/role), color by type
│ ├── OidcBadge.tsx ← Small badge showing OIDC provider origin
│ ├── InheritanceDiagram.tsx ← Three-column Groups→Roles→Users read-only diagram
│ └── InheritanceNote.tsx ← Green-bordered explanation block
api/queries/admin/
│ └── rbac.ts ← useUsers, useUser, useGroups, useGroup, useRoles, useRole, useRbacStats + mutation hooks
```
### Tab Navigation
`RbacPage` uses a horizontal tab bar (Dashboard | Users | Groups | Roles) with URL-synced active state via query parameter (`?tab=users`). Each tab renders its content below the tab bar in the full main panel area.
### Split Pane Layout
Users, Groups, and Roles tabs share the same layout:
- **Left (52%)**: `EntityListPane` with search input + scrollable entity cards
- **Right (48%)**: Detail pane showing selected entity, or empty state prompt
- Resizable via `ResizableDivider` (existing shared component)
### Entity Card Patterns
**User card:** Circle avatar (initials, blue tint) + name + email/primary-group meta + role tags (amber) + group tags (green) + status dot + OIDC badge if `provider !== "local"`
**Group card:** Rounded-square avatar (initials, green/amber/red by domain) + name + parent/member-count meta + role tags (direct solid, inherited faded+italic)
**Role card:** Rounded-square avatar (initials, amber tint) + name + description/assignment-count meta + assigned-to tags. System roles show a lock icon.
### Badge/Chip Styling
Following the spec and existing CSS token system:
| Chip type | Background | Border | Text |
|---|---|---|---|
| Role (direct) | `var(--amber-dim)` | solid `var(--amber)` | amber text |
| Role (inherited) | transparent | dashed `var(--amber)` | faded amber, italic |
| Group | `var(--green-dim)` / `#E1F5EE` | solid green | green text |
| OIDC badge | `var(--cyan-dim)` | solid cyan | cyan text, shows provider |
| System role | Same as role but with lock icon | — | — |
Inherited role chips include `↑ GroupName` annotation in the detail pane.
### OIDC Badge
Displayed on user cards and user detail when `provider !== "local"`. Shows a small cyan-tinted pill with the provider name (e.g., "OIDC" or the issuer hostname). Positioned after the user's name in the card, and as a field in the detail pane.
### Search
Client-side filtering on entity list panes — filter by any visible text (name, email, group, role). Sufficient for the expected user count.
### State Management
- React Query for all server state (users, groups, roles, stats)
- Local `useState` for selected entity, search filter, active tab
- Mutations invalidate related queries (e.g., updating a user's groups invalidates both user and group queries)
---
## Migration Strategy
1. Delete all V1V10 migration files
2. Create single `V1__init.sql` containing the full consolidated schema
3. Deployed environments: drop and recreate the database (data loss accepted)
4. CI/CD: no special handling — clean database on deploy
5. Update `application.yml` if needed: `spring.flyway.clean-on-validation-error: true` or manual DB drop
---
## Out of Scope
- Permission-based access control (custom roles don't gate endpoints — system roles do)
- Audit log panel within RBAC (existing audit log page covers this)
- Bulk import/export of users or groups
- SCIM provisioning
- Role negation / deny rules

View File

@@ -0,0 +1,261 @@
# Cameleer3 Dashboard Review -- Senior Camel Developer Perspective
**Reviewer**: Senior Apache Camel Developer (10+ years, Java DSL / Spring Boot)
**Artifact reviewed**: `mock-v2-light.html` -- Operations Dashboard (v2 synthesis)
**Date**: 2026-03-17
---
## 1. What the Dashboard Gets RIGHT
### Business ID as First-Class Citizen
The Order ID and Customer columns in the execution table are exactly what I need. When support calls me about "order OP-88421", I can paste that into the search and find the execution immediately. Every other monitoring tool I have used forces me to map business IDs to correlation IDs manually. This alone would save me 10-15 minutes per incident.
### Inline Error Previews
Showing the exception message directly in the table row without requiring a click-through is genuinely useful. The two error examples in the mock (`HttpOperationFailedException` with a 504, `SQLTransientConnectionException` with HikariPool exhaustion) are realistic Camel exceptions. I can scan the error list and immediately tell whether it is a downstream timeout or a connection pool issue. That distinction determines whether I investigate our code or page the DBA.
### Processor Timeline (Gantt View)
The processor timeline in the detail panel is the single most valuable feature. Seeing that `to(payment-api)` consumed 280ms out of a 412ms total execution, while `enrich(inventory)` took 85ms, immediately tells me WHERE the bottleneck is. In my experience, 95% of Camel performance issues are in external calls, and this view pinpoints them. The color coding (green/yellow/red) for processor bars makes the slow step obvious at a glance.
### SLA Awareness Baked In
The SLA threshold line on the latency chart, the "SLA" tag on slow durations, and the "CLOSE" warning on the p99 card are exactly the kind of proactive indicators I want. Most monitoring tools show me raw numbers; this dashboard shows me numbers in context. I know immediately that 287ms p99 is dangerously close to our 300ms SLA.
### Shift-Aware Time Context
The "since 06:00" shift concept is something I have never seen in a developer tool but actually matches how production support works. When I start my day shift, I want to see what happened overnight and what is happening now, not a rolling 24-hour window that mixes yesterday afternoon with this morning.
### Agent Health in Sidebar
Seeing agent status (live/stale/dead), throughput per agent, and error rates at a glance in the sidebar is practical. When an agent goes stale, I know to check if a pod restarted or if there is a network partition.
### Application-to-Route Navigation Hierarchy
The sidebar tree (Applications > order-service > Routes > order-intake, order-enrichment, etc.) matches how I think about Camel deployments. I have multiple applications, each with multiple routes. Being able to filter by application first, then drill into routes, is the right hierarchy.
---
## 2. What is MISSING or Could Be Better
### 2.1 Exchange Body/Header Inspection -- CRITICAL GAP
**Pain point**: The "Exchange" tab exists in the detail panel tabs but its content is not shown. This is the single most important debugging feature for a Camel developer. When a message fails at step 5 of 7, I need to see:
- What was the original inbound message (before any transformation)?
- What did the exchange body look like at each processor step?
- Which headers were present at each step, and which were added/removed?
- What was the exception body (often different from the exception message)?
**How to address it**: The Exchange tab should show a step-by-step diff view of the exchange. For each processor in the route, show the body (with a JSON/XML pretty-printer) and the headers map. Highlight headers that were added at that step. Allow comparing any two steps side-by-side. Show the original inbound message prominently at the top.
**Priority**: **Must-Have**. Without this, the dashboard is an operations monitor, not a debugging tool. This is the difference between "I can see something failed" and "I can see WHY it failed."
### 2.2 Route Diagram / Visual Graph -- MENTIONED BUT NOT SHOWN
**Pain point**: The "View Route Diagram" button exists in the detail actions, but there is no mockup of what the route diagram looks like. As a Camel developer, I need to see the DAG (directed acyclic graph) of my route: from(jms:orders) -> unmarshal -> validate -> choice -> [branch A: enrich -> transform -> to(http)] [branch B: log -> to(dlq)]. I also need to see execution overlay on the diagram -- which path did THIS specific exchange take, and how long did each node take.
**How to address it**: Add a Route Diagram page/view that shows:
- The route definition as an interactive DAG (nodes = processors, edges = flow)
- Execution overlay: color-code each node by success/failure for a specific execution
- Aggregate overlay: color-code each node by throughput/error rate over a time window
- Highlight the path taken by the selected exchange (dim the branches not taken)
- Show inter-route connections (e.g., `direct:`, `seda:`, `vm:` endpoints linking routes)
**Priority**: **Must-Have**. Cameleer already has `RouteGraph` data from agents -- this is the tool's differentiating feature.
### 2.3 Cross-Route Correlation / Message Tracing
**Pain point**: A single business transaction (e.g., an order) often spans multiple routes: `order-intake` -> `order-enrichment` -> `payment-process` -> `shipment-dispatch`. The dashboard shows each route execution as a separate row. There is no way to see the full journey of order OP-88421 across all routes.
**How to address it**: Add a "Transaction Trace" or "Message Flow" view that:
- Groups all executions sharing a breadcrumbId or correlation ID
- Shows them as a horizontal timeline or waterfall chart
- Highlights which route in the chain failed
- Works across `direct:`, `seda:`, and `vm:` endpoints that link routes
The search bar says "Search by Order ID, correlation ID" which is a good start, but the results should show the correlated group, not just individual rows.
**Priority**: **Must-Have**. Splitter/aggregator patterns and multi-route flows are the norm, not the exception, in real Camel applications.
### 2.4 Dead Letter Queue Monitoring
**Pain point**: When messages fail and are routed to a dead letter channel (which is the standard Camel error handling pattern), I need to know: how many messages are in the DLQ, what are they, how long have they been there, and can I retry them?
**How to address it**: Add a DLQ section or page showing:
- Count of messages per dead letter endpoint
- Age distribution (how many are from today vs. last week)
- Message preview (body + headers + the exception that caused routing to DLQ)
- Retry action (re-submit the message to the original route)
- Purge action (acknowledge and discard)
**Priority**: **Must-Have**. DLQ management is a daily production task.
### 2.5 Per-Processor Statistics (Aggregate View)
**Pain point**: The processor timeline in the detail panel shows per-processor timing for a single execution. But I also need aggregate statistics: for processor `to(payment-api)`, what is the p50/p95/p99 latency over the last hour? How many times did it fail? Is it getting slower over time?
**How to address it**: Clicking a processor name in the timeline should show aggregate stats for that processor. Alternatively, the Route Detail page should have a "Processors" tab with a table of all processors in the route, their call count, success rate, and latency percentiles.
**Priority**: **Must-Have**. Identifying a chronically slow processor is different from identifying a one-off slow execution.
### 2.6 Error Pattern Grouping / Top Errors
**Pain point**: The dashboard shows individual error rows. When there are 38 errors, I do not want to scroll through all 38. I want to see: "23 of the 38 errors are `HttpOperationFailedException` on `payment-process`, 10 are `SQLTransientConnectionException` on `order-enrichment`, 5 are `ValidationException` on `order-intake`." The design notes mention "Top error pattern grouping panel" from the operator expert, but it is not in the final mock.
**How to address it**: Add an error summary panel above or alongside the execution table showing errors grouped by exception class + route. Each group should show count, first/last occurrence, and whether the count is trending up.
**Priority**: **Must-Have**. Pattern recognition is more important than individual error viewing.
### 2.7 Route Status Management
**Pain point**: I need to know which routes are started, stopped, or suspended. And I need the ability to stop/start/suspend individual routes without redeploying. This is routine in production -- temporarily suspending a route that is flooding a downstream system.
**How to address it**: The sidebar route list should show route status (started/stopped/suspended) with icons. Right-click or action menu on a route should offer start/stop/suspend. This maps directly to Camel's route controller API.
**Priority**: **Nice-to-Have** for v1, **Must-Have** for v2. Operators will ask for this quickly.
### 2.8 Route Version Comparison
**Pain point**: After a deployment, I want to compare the current route definition with the previous version. Did someone add a processor? Change an endpoint URI? Route definition drift is a real source of production issues.
**How to address it**: Store route graph snapshots per deployment/version. Show a diff view highlighting added/removed/modified processors.
**Priority**: **Nice-to-Have**. Valuable but less urgent than the above.
### 2.9 Thread Pool / Resource Monitoring
**Pain point**: Camel's default thread pool max is 20. When all threads are consumed, messages queue up silently. The HikariPool error in the mock is a perfect example -- pool exhaustion. I need visibility into thread pool utilization, connection pool utilization, and inflight exchange count.
**How to address it**: Add a "Resources" section (either in the agent detail or a separate page) showing:
- Camel thread pool utilization (active/max)
- Connection pool utilization (from endpoint components)
- Inflight exchange count per route
- Consumer prefetch/backlog (for JMS/Kafka consumers)
**Priority**: **Nice-to-Have** initially, but becomes **Must-Have** when debugging pool exhaustion issues.
### 2.10 Saved Searches / Alert Rules
**Pain point**: I find myself searching for the same patterns repeatedly: "errors on payment-process in the last hour", "executions over 500ms for order-enrichment". There is no way to save these as bookmarks or convert them into alert rules.
**How to address it**: Allow saving filter configurations as named views. Allow converting a saved search into an alerting rule (email/webhook when count exceeds threshold).
**Priority**: **Nice-to-Have**.
---
## 3. Specific Page/Feature Recommendations
### 3.1 Route Detail Page
When I click a route name (e.g., `order-intake`) from the sidebar, I should see:
- **Header**: Route name, status (started/stopped), uptime, route definition source (Java DSL / XML / YAML)
- **KPI Strip**: Total executions, success rate, p50/p99 latency, inflight count, throughput -- all for this route only
- **Processor Table**: Every processor in the route with columns: name, type, call count, success rate, p50 latency, p99 latency, total time %. Sortable by any column. This is where I find the bottleneck processor.
- **Route Diagram**: Interactive DAG with execution overlay. Nodes sized by throughput, colored by error rate. Clicking a node filters the execution list to that processor.
- **Recent Executions**: Filtered version of the main table, showing only this route's executions.
- **Error Patterns**: Top errors for this route, grouped by exception class.
### 3.2 Exchange / Message Inspector
When I click "Exchange" tab in the detail panel:
- **Inbound Message**: The original message as received by the route's consumer. Body + headers. Shown prominently, always visible.
- **Step-by-Step Trace**: For each processor, show the exchange state AFTER that processor ran. Diff mode should highlight what changed (body mutations, added headers, removed headers).
- **Properties**: Camel exchange properties (not just headers). Properties often carry routing decisions.
- **Exception**: If the exchange failed, show the caught exception, the handled flag, and whether it was routed to a dead letter channel.
- **Response**: If the route produces a response (e.g., REST endpoint), show the outbound body.
Display format should auto-detect JSON/XML and pretty-print. Binary payloads should show hex dump with size.
### 3.3 Metrics Dashboard (Developer vs. Operator KPIs)
The current metrics (throughput, latency p99, error rate) are operator KPIs. A Camel developer also needs:
**Developer KPIs** (add a "Developer" metrics view):
- Per-processor latency breakdown (stacked bar: which processors consume the most time)
- External endpoint response time (HTTP, DB, JMS) -- separate from Camel processing time
- Type converter cache hit rate (rarely needed, but valuable when debugging serialization issues)
- Redelivery count (how many messages required retries before succeeding)
- Content-based router distribution (for `choice()` routes: how many messages went down each branch)
**Operator KPIs** (already well-covered):
- Throughput, error rate, latency percentiles -- these are solid as-is
### 3.4 Dead Letter Queue View
A dedicated DLQ page:
- **Summary Cards**: One card per DLQ endpoint (e.g., `jms:DLQ.orders`, `seda:error-handler`), showing message count, oldest message age, newest message timestamp.
- **Message List**: Table with columns: original route, exception class, business ID, timestamp, retry count.
- **Message Detail**: Click a DLQ message to see the exchange snapshot (body + headers + exception) at the time of failure.
- **Actions**: Retry (re-submit to original endpoint), Retry All (bulk retry for a pattern), Discard, Move to another queue.
- **Filters**: By exception type, by route, by age.
### 3.5 Route Comparison
Two use cases:
1. **Version diff**: Compare route graph v3.2.0 vs. v3.2.1. Show added/removed/modified processors as a visual diff on the DAG.
2. **Performance comparison**: Compare this week's latency distribution for `payment-process` with last week's. Overlay histograms. Useful for validating that a deployment improved (or degraded) performance.
---
## 4. Information Architecture Critique
### What Works
- **Sidebar hierarchy** (Applications > Routes) is correct and matches how Camel projects are structured.
- **Health strip at top** provides instant situational awareness without scrolling.
- **Master-detail pattern** (table + slide-in panel) avoids page navigation for quick inspection. This keeps context.
- **Keyboard shortcuts** (Ctrl+K search, arrow navigation, Esc to close) are the right accelerators for power users.
### What Needs Adjustment
**The sidebar is too flat.** It shows applications and routes in the same list, but there is no way to navigate to:
- A dedicated Route Detail page (with per-processor stats, diagram, error patterns)
- An Agent Detail page (with resource utilization, version info, configuration)
- A DLQ page
- A Search/Trace page (for cross-route correlation)
Recommendation: Add top-level navigation items to the sidebar:
```
Dashboard (the current view)
Routes (route list with status, drill into route detail)
Traces (cross-route message flow / correlation)
Errors (grouped error patterns, DLQ)
Agents (agent health, resource utilization)
Diagrams (route graph visualization)
```
**Route click should go deeper.** Currently, clicking a route in the sidebar filters the execution table. This is useful, but clicking the route NAME in a table row or in the detail panel should navigate to a dedicated Route Detail page with per-processor aggregate stats and the route diagram.
**Search results need grouping.** The Ctrl+K search bar says "Search by Order ID, route, error..." but search results should group by correlation ID when searching by business ID. If I search for "OP-88421", I want to see ALL executions related to that order across all routes, not just the one row in `payment-process`.
**1-click access priorities:**
- Health overview: 1 click (current: 0 clicks -- it is the home page -- good)
- Filter by errors only: 1 click (current: 1 click on Error pill -- good)
- View a specific execution's processor timeline: 2 clicks (current: 1 click on row -- good)
- View exchange body/headers: should be 2 clicks (click row, click Exchange tab). Currently not implemented.
- View route diagram: should be 2 clicks (click route name, see diagram). Currently requires finding the button in the detail panel.
- Cross-route trace: should be 2 clicks (click correlation ID or business ID, see trace). Currently not possible.
- DLQ status: should be 1 click from sidebar. Currently not available.
---
## 5. Score Card
| Dimension | Score (1-10) | Notes |
|-----------------------------|:---:|-------|
| Transaction tracking | 4 | Individual executions visible, but no cross-route transaction view. Correlation ID shown but not actionable. |
| Root cause analysis | 6 | Processor timeline identifies the slow/failing step. Error messages shown inline. But no exchange body inspection, no stack trace expansion, no header diff. |
| Performance monitoring | 7 | Throughput, latency p99, error rate charts with SLA lines are solid. Missing per-processor aggregate stats and resource utilization. |
| Route visualization | 3 | Route names in sidebar, but no actual route diagram/DAG. The "View Route Diagram" button exists with no destination. This is Cameleer's key differentiator -- it must ship. |
| Exchange/message visibility | 2 | Exchange tab exists but has no content. No body inspection, no header view, no step-by-step diff. This is the most critical gap. |
| Correlation/tracing | 3 | Correlation ID displayed in detail panel, but no way to trace a message across routes. No breadcrumb linking. No transaction waterfall. |
| Overall daily usefulness | 5 | As an operations monitor (is anything broken right now?), it scores 7-8. As a developer debugging tool (why is it broken and how do I fix it?), it scores 3-4. The gap is in the debugging/inspection features. |
### Summary Verdict
The dashboard is a **strong operations monitor** -- it answers "what is happening right now?" effectively. The health strip, SLA awareness, shift context, business ID columns, and inline error previews are genuinely useful and better than most tools I have used.
However, it is a **weak debugging tool** -- it does not yet answer "why did this specific message fail?" or "what did the exchange look like at each step?" The Exchange tab, route diagram, cross-route tracing, and error pattern grouping are the features that would make this a daily-driver tool rather than a pretty overview I glance at in the morning.
The processor Gantt chart in the detail panel is the single best feature in the entire dashboard. Build on that. Make it clickable (click a processor to see the exchange state at that point). Add aggregate stats. Link it to the route diagram. That is where this tool becomes indispensable.
**Bottom line**: Ship the exchange inspector, the route diagram, and cross-route tracing, and this goes from a 5/10 to an 8/10 daily-use tool.

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

BIN
live-agents-v2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

BIN
live-agents.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

BIN
live-dashboard-detail.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 218 KiB

BIN
live-dashboard-panel-v2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 223 KiB

BIN
live-dashboard-panel-v3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 150 KiB

BIN
live-dashboard-panel.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 240 KiB

BIN
live-dashboard-v2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 184 KiB

BIN
live-dashboard-v3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 210 KiB

BIN
live-dashboard.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 150 KiB

BIN
live-routes-v2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 104 KiB

BIN
mock-agent-instance.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 165 KiB

BIN
mock-agents.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 138 KiB

BIN
mock-dashboard-v2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

BIN
mock-dashboard.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

BIN
mock-routes.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 174 KiB

View File

@@ -6,10 +6,10 @@ export function useExecutionStats(
timeFrom: string | undefined, timeFrom: string | undefined,
timeTo: string | undefined, timeTo: string | undefined,
routeId?: string, routeId?: string,
group?: string, application?: string,
) { ) {
return useQuery({ return useQuery({
queryKey: ['executions', 'stats', timeFrom, timeTo, routeId, group], queryKey: ['executions', 'stats', timeFrom, timeTo, routeId, application],
queryFn: async () => { queryFn: async () => {
const { data, error } = await api.GET('/search/stats', { const { data, error } = await api.GET('/search/stats', {
params: { params: {
@@ -17,7 +17,7 @@ export function useExecutionStats(
from: timeFrom!, from: timeFrom!,
to: timeTo || undefined, to: timeTo || undefined,
routeId: routeId || undefined, routeId: routeId || undefined,
group: group || undefined, application: application || undefined,
}, },
}, },
}); });
@@ -49,10 +49,10 @@ export function useStatsTimeseries(
timeFrom: string | undefined, timeFrom: string | undefined,
timeTo: string | undefined, timeTo: string | undefined,
routeId?: string, routeId?: string,
group?: string, application?: string,
) { ) {
return useQuery({ return useQuery({
queryKey: ['executions', 'timeseries', timeFrom, timeTo, routeId, group], queryKey: ['executions', 'timeseries', timeFrom, timeTo, routeId, application],
queryFn: async () => { queryFn: async () => {
const { data, error } = await api.GET('/search/stats/timeseries', { const { data, error } = await api.GET('/search/stats/timeseries', {
params: { params: {
@@ -61,7 +61,7 @@ export function useStatsTimeseries(
to: timeTo || undefined, to: timeTo || undefined,
buckets: 24, buckets: 24,
routeId: routeId || undefined, routeId: routeId || undefined,
group: group || undefined, application: application || undefined,
}, },
}, },
}); });

View File

@@ -1079,7 +1079,7 @@ export interface components {
routeId?: string; routeId?: string;
agentId?: string; agentId?: string;
processorType?: string; processorType?: string;
group?: string; application?: string;
agentIds?: string[]; agentIds?: string[];
/** Format: int32 */ /** Format: int32 */
offset?: number; offset?: number;
@@ -1092,7 +1092,7 @@ export interface components {
executionId: string; executionId: string;
routeId: string; routeId: string;
agentId: string; agentId: string;
groupName: string; applicationName: string;
status: string; status: string;
/** Format: date-time */ /** Format: date-time */
startTime: string; startTime: string;
@@ -1327,7 +1327,7 @@ export interface components {
errorStackTrace: string; errorStackTrace: string;
diagramContentHash: string; diagramContentHash: string;
processors: components["schemas"]["ProcessorNode"][]; processors: components["schemas"]["ProcessorNode"][];
groupName?: string; applicationName?: string;
children?: components["schemas"]["ProcessorNode"][]; children?: components["schemas"]["ProcessorNode"][];
}; };
ProcessorNode: { ProcessorNode: {
@@ -2977,7 +2977,7 @@ export interface operations {
from: string; from: string;
to?: string; to?: string;
routeId?: string; routeId?: string;
group?: string; application?: string;
}; };
header?: never; header?: never;
path?: never; path?: never;
@@ -3003,7 +3003,7 @@ export interface operations {
to?: string; to?: string;
buckets?: number; buckets?: number;
routeId?: string; routeId?: string;
group?: string; application?: string;
}; };
header?: never; header?: never;
path?: never; path?: never;

View File

@@ -36,7 +36,7 @@ export default function Dashboard() {
const { data: searchResult } = useSearchExecutions({ const { data: searchResult } = useSearchExecutions({
timeFrom, timeTo, timeFrom, timeTo,
routeId: routeId || undefined, routeId: routeId || undefined,
group: appId || undefined, application: appId || undefined,
offset: 0, limit: 50, offset: 0, limit: 50,
}, true); }, true);
const { data: detail } = useExecutionDetail(selectedId); const { data: detail } = useExecutionDetail(selectedId);
@@ -94,7 +94,7 @@ export default function Dashboard() {
), ),
}, },
{ key: 'routeId', header: 'Route', sortable: true, render: (v) => <span>{String(v)}</span> }, { key: 'routeId', header: 'Route', sortable: true, render: (v) => <span>{String(v)}</span> },
{ key: 'groupName', header: 'Application', sortable: true, render: (v) => <span>{String(v ?? '')}</span> }, { key: 'applicationName', header: 'Application', sortable: true, render: (v) => <span>{String(v ?? '')}</span> },
{ key: 'executionId', header: 'Exchange ID', sortable: true, render: (v) => <MonoText size="xs">{String(v)}</MonoText> }, { key: 'executionId', header: 'Exchange ID', sortable: true, render: (v) => <MonoText size="xs">{String(v)}</MonoText> },
{ key: 'startTime', header: 'Started', sortable: true, render: (v) => <MonoText size="xs">{new Date(v as string).toISOString().replace('T', ' ').slice(0, 19)}</MonoText> }, { key: 'startTime', header: 'Started', sortable: true, render: (v) => <MonoText size="xs">{new Date(v as string).toISOString().replace('T', ' ').slice(0, 19)}</MonoText> },
{ {

View File

@@ -94,7 +94,7 @@ export default function ExchangeDetail() {
<div> <div>
<Breadcrumb items={[ <Breadcrumb items={[
{ label: 'Dashboard', href: '/apps' }, { label: 'Dashboard', href: '/apps' },
{ label: detail.groupName || 'App', href: `/apps/${detail.groupName}` }, { label: detail.applicationName || 'App', href: `/apps/${detail.applicationName}` },
{ label: id?.slice(0, 12) || '' }, { label: id?.slice(0, 12) || '' },
]} /> ]} />
@@ -109,11 +109,11 @@ export default function ExchangeDetail() {
<Badge label={detail.status} color={detail.status === 'COMPLETED' ? 'success' : 'error'} variant="filled" /> <Badge label={detail.status} color={detail.status === 'COMPLETED' ? 'success' : 'error'} variant="filled" />
</div> </div>
<div className={styles.exchangeRoute}> <div className={styles.exchangeRoute}>
Route: <span className={styles.routeLink} onClick={() => navigate(`/apps/${detail.groupName}/${detail.routeId}`)}>{detail.routeId}</span> Route: <span className={styles.routeLink} onClick={() => navigate(`/apps/${detail.applicationName}/${detail.routeId}`)}>{detail.routeId}</span>
{detail.groupName && ( {detail.applicationName && (
<> <>
<span className={styles.headerDivider}>&middot;</span> <span className={styles.headerDivider}>&middot;</span>
App: <MonoText size="xs">{detail.groupName}</MonoText> App: <MonoText size="xs">{detail.applicationName}</MonoText>
</> </>
)} )}
</div> </div>

View File

@@ -51,7 +51,7 @@ export default function RouteDetail() {
timeFrom, timeFrom,
timeTo, timeTo,
routeId: routeId || undefined, routeId: routeId || undefined,
group: appId || undefined, application: appId || undefined,
offset: 0, offset: 0,
limit: 50, limit: 50,
}); });
@@ -59,7 +59,7 @@ export default function RouteDetail() {
timeFrom, timeFrom,
timeTo, timeTo,
routeId: routeId || undefined, routeId: routeId || undefined,
group: appId || undefined, application: appId || undefined,
status: 'FAILED', status: 'FAILED',
offset: 0, offset: 0,
limit: 200, limit: 200,

View File

@@ -0,0 +1 @@
{"root":["./src/config.ts","./src/main.tsx","./src/router.tsx","./src/swagger-ui-dist.d.ts","./src/vite-env.d.ts","./src/api/client.ts","./src/api/schema.d.ts","./src/api/types.ts","./src/api/queries/agents.ts","./src/api/queries/catalog.ts","./src/api/queries/diagrams.ts","./src/api/queries/executions.ts","./src/api/queries/admin/admin-api.ts","./src/api/queries/admin/audit.ts","./src/api/queries/admin/database.ts","./src/api/queries/admin/opensearch.ts","./src/api/queries/admin/rbac.ts","./src/api/queries/admin/thresholds.ts","./src/auth/loginpage.tsx","./src/auth/oidccallback.tsx","./src/auth/protectedroute.tsx","./src/auth/auth-store.ts","./src/auth/use-auth.ts","./src/components/layoutshell.tsx","./src/pages/admin/auditlogpage.tsx","./src/pages/admin/databaseadminpage.tsx","./src/pages/admin/oidcconfigpage.tsx","./src/pages/admin/opensearchadminpage.tsx","./src/pages/admin/rbacpage.tsx","./src/pages/agenthealth/agenthealth.tsx","./src/pages/agentinstance/agentinstance.tsx","./src/pages/dashboard/dashboard.tsx","./src/pages/exchangedetail/exchangedetail.tsx","./src/pages/routes/routesmetrics.tsx","./src/pages/swagger/swaggerpage.tsx"],"version":"5.9.3"}

View File

@@ -0,0 +1 @@
{"root":["./vite.config.ts"],"version":"5.9.3"}

1
ui/tsconfig.tsbuildinfo Normal file
View File

@@ -0,0 +1 @@
{"root":["./vite.config.ts","./src/config.ts","./src/main.tsx","./src/router.tsx","./src/swagger-ui-dist.d.ts","./src/vite-env.d.ts","./src/api/client.ts","./src/api/schema.d.ts","./src/api/types.ts","./src/api/queries/agents.ts","./src/api/queries/catalog.ts","./src/api/queries/diagrams.ts","./src/api/queries/executions.ts","./src/api/queries/admin/admin-api.ts","./src/api/queries/admin/audit.ts","./src/api/queries/admin/database.ts","./src/api/queries/admin/opensearch.ts","./src/api/queries/admin/rbac.ts","./src/api/queries/admin/thresholds.ts","./src/auth/loginpage.tsx","./src/auth/oidccallback.tsx","./src/auth/protectedroute.tsx","./src/auth/auth-store.ts","./src/auth/use-auth.ts","./src/components/layoutshell.tsx","./src/pages/admin/auditlogpage.tsx","./src/pages/admin/databaseadminpage.tsx","./src/pages/admin/oidcconfigpage.tsx","./src/pages/admin/opensearchadminpage.tsx","./src/pages/admin/rbacpage.tsx","./src/pages/agenthealth/agenthealth.tsx","./src/pages/agentinstance/agentinstance.tsx","./src/pages/dashboard/dashboard.tsx","./src/pages/exchangedetail/exchangedetail.tsx","./src/pages/routes/routesmetrics.tsx","./src/pages/swagger/swaggerpage.tsx"],"errors":true,"version":"5.9.3"}