refactor: rename group/groupName to application/applicationName
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>
@@ -53,11 +53,11 @@ public class ExecutionController {
|
||||
@ApiResponse(responseCode = "202", description = "Data accepted for processing")
|
||||
public ResponseEntity<Void> ingestExecutions(@RequestBody String body) throws JsonProcessingException {
|
||||
String agentId = extractAgentId();
|
||||
String groupName = resolveGroupName(agentId);
|
||||
String applicationName = resolveApplicationName(agentId);
|
||||
List<RouteExecution> executions = parsePayload(body);
|
||||
|
||||
for (RouteExecution execution : executions) {
|
||||
ingestionService.ingestExecution(agentId, groupName, execution);
|
||||
ingestionService.ingestExecution(agentId, applicationName, execution);
|
||||
}
|
||||
|
||||
return ResponseEntity.accepted().build();
|
||||
@@ -68,7 +68,7 @@ public class ExecutionController {
|
||||
return auth != null ? auth.getName() : "";
|
||||
}
|
||||
|
||||
private String resolveGroupName(String agentId) {
|
||||
private String resolveApplicationName(String agentId) {
|
||||
AgentInfo agent = registryService.findById(agentId);
|
||||
return agent != null ? agent.group() : "";
|
||||
}
|
||||
|
||||
@@ -65,7 +65,7 @@ public class RouteMetricsController {
|
||||
List<RouteKey> routeKeys = new ArrayList<>();
|
||||
|
||||
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");
|
||||
long total = rs.getLong("total");
|
||||
long failed = rs.getLong("failed");
|
||||
@@ -76,8 +76,8 @@ public class RouteMetricsController {
|
||||
double errorRate = total > 0 ? (double) failed / total : 0.0;
|
||||
double tps = windowSeconds > 0 ? (double) total / windowSeconds : 0.0;
|
||||
|
||||
routeKeys.add(new RouteKey(groupName, routeId));
|
||||
return new RouteMetrics(routeId, groupName, total, successRate,
|
||||
routeKeys.add(new RouteKey(applicationName, routeId));
|
||||
return new RouteMetrics(routeId, applicationName, total, successRate,
|
||||
avgDur, p99Dur, errorRate, tps, List.of());
|
||||
}, params.toArray());
|
||||
|
||||
|
||||
@@ -51,13 +51,13 @@ public class SearchController {
|
||||
@RequestParam(required = false) String routeId,
|
||||
@RequestParam(required = false) String agentId,
|
||||
@RequestParam(required = false) String processorType,
|
||||
@RequestParam(required = false) String group,
|
||||
@RequestParam(required = false) String application,
|
||||
@RequestParam(defaultValue = "0") int offset,
|
||||
@RequestParam(defaultValue = "50") int limit,
|
||||
@RequestParam(required = false) String sortField,
|
||||
@RequestParam(required = false) String sortDir) {
|
||||
|
||||
List<String> agentIds = resolveGroupToAgentIds(group);
|
||||
List<String> agentIds = resolveApplicationToAgentIds(application);
|
||||
|
||||
SearchRequest request = new SearchRequest(
|
||||
status, timeFrom, timeTo,
|
||||
@@ -65,7 +65,7 @@ public class SearchController {
|
||||
correlationId,
|
||||
text, null, null, null,
|
||||
routeId, agentId, processorType,
|
||||
group, agentIds,
|
||||
application, agentIds,
|
||||
offset, limit,
|
||||
sortField, sortDir
|
||||
);
|
||||
@@ -77,11 +77,11 @@ public class SearchController {
|
||||
@Operation(summary = "Advanced search with all filters")
|
||||
public ResponseEntity<SearchResult<ExecutionSummary>> searchPost(
|
||||
@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;
|
||||
if (request.group() != null && !request.group().isBlank()
|
||||
if (request.application() != null && !request.application().isBlank()
|
||||
&& (request.agentIds() == null || request.agentIds().isEmpty())) {
|
||||
resolved = request.withAgentIds(resolveGroupToAgentIds(request.group()));
|
||||
resolved = request.withAgentIds(resolveApplicationToAgentIds(request.application()));
|
||||
}
|
||||
return ResponseEntity.ok(searchService.search(resolved));
|
||||
}
|
||||
@@ -92,9 +92,9 @@ public class SearchController {
|
||||
@RequestParam Instant from,
|
||||
@RequestParam(required = false) Instant to,
|
||||
@RequestParam(required = false) String routeId,
|
||||
@RequestParam(required = false) String group) {
|
||||
@RequestParam(required = false) String application) {
|
||||
Instant end = to != null ? to : Instant.now();
|
||||
List<String> agentIds = resolveGroupToAgentIds(group);
|
||||
List<String> agentIds = resolveApplicationToAgentIds(application);
|
||||
if (routeId == null && agentIds == null) {
|
||||
return ResponseEntity.ok(searchService.stats(from, end));
|
||||
}
|
||||
@@ -108,9 +108,9 @@ public class SearchController {
|
||||
@RequestParam(required = false) Instant to,
|
||||
@RequestParam(defaultValue = "24") int buckets,
|
||||
@RequestParam(required = false) String routeId,
|
||||
@RequestParam(required = false) String group) {
|
||||
@RequestParam(required = false) String application) {
|
||||
Instant end = to != null ? to : Instant.now();
|
||||
List<String> agentIds = resolveGroupToAgentIds(group);
|
||||
List<String> agentIds = resolveApplicationToAgentIds(application);
|
||||
if (routeId == null && agentIds == null) {
|
||||
return ResponseEntity.ok(searchService.timeseries(from, end, buckets));
|
||||
}
|
||||
@@ -118,14 +118,14 @@ public class SearchController {
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve an application group name to agent IDs.
|
||||
* Returns null if group is null/blank (no filtering).
|
||||
* Resolve an application name to agent IDs.
|
||||
* Returns null if application is null/blank (no filtering).
|
||||
*/
|
||||
private List<String> resolveGroupToAgentIds(String group) {
|
||||
if (group == null || group.isBlank()) {
|
||||
private List<String> resolveApplicationToAgentIds(String application) {
|
||||
if (application == null || application.isBlank()) {
|
||||
return null;
|
||||
}
|
||||
return registryService.findByGroup(group).stream()
|
||||
return registryService.findByGroup(application).stream()
|
||||
.map(AgentInfo::id)
|
||||
.toList();
|
||||
}
|
||||
|
||||
@@ -288,7 +288,7 @@ public class OpenSearchIndex implements SearchIndex {
|
||||
map.put("execution_id", doc.executionId());
|
||||
map.put("route_id", doc.routeId());
|
||||
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("correlation_id", doc.correlationId());
|
||||
map.put("exchange_id", doc.exchangeId());
|
||||
|
||||
@@ -45,7 +45,7 @@ public class PostgresExecutionStore implements ExecutionStore {
|
||||
updated_at = now()
|
||||
""",
|
||||
execution.executionId(), execution.routeId(), execution.agentId(),
|
||||
execution.groupName(), execution.status(), execution.correlationId(),
|
||||
execution.applicationName(), execution.status(), execution.correlationId(),
|
||||
execution.exchangeId(),
|
||||
Timestamp.from(execution.startTime()),
|
||||
execution.endTime() != null ? Timestamp.from(execution.endTime()) : null,
|
||||
@@ -55,7 +55,7 @@ public class PostgresExecutionStore implements ExecutionStore {
|
||||
|
||||
@Override
|
||||
public void upsertProcessors(String executionId, Instant startTime,
|
||||
String groupName, String routeId,
|
||||
String applicationName, String routeId,
|
||||
List<ProcessorRecord> processors) {
|
||||
jdbc.batchUpdate("""
|
||||
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[]{
|
||||
p.executionId(), p.processorId(), p.processorType(),
|
||||
p.diagramNodeId(), p.groupName(), p.routeId(),
|
||||
p.diagramNodeId(), p.applicationName(), p.routeId(),
|
||||
p.depth(), p.parentProcessorId(), p.status(),
|
||||
Timestamp.from(p.startTime()),
|
||||
p.endTime() != null ? Timestamp.from(p.endTime()) : null,
|
||||
|
||||
@@ -29,9 +29,9 @@ public class PostgresStatsStore implements StatsStore {
|
||||
}
|
||||
|
||||
@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(
|
||||
new Filter("group_name", groupName)));
|
||||
new Filter("group_name", applicationName)));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -56,9 +56,9 @@ public class PostgresStatsStore implements StatsStore {
|
||||
}
|
||||
|
||||
@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(
|
||||
new Filter("group_name", groupName)), true);
|
||||
new Filter("group_name", applicationName)), true);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -54,10 +54,10 @@ class PostgresStatsStoreIT extends AbstractPostgresIT {
|
||||
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) {
|
||||
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,
|
||||
status.equals("FAILED") ? "error" : null, null, null));
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ public class DetailService {
|
||||
List<ProcessorNode> roots = buildTree(processors);
|
||||
return new ExecutionDetail(
|
||||
exec.executionId(), exec.routeId(), exec.agentId(),
|
||||
exec.groupName(),
|
||||
exec.applicationName(),
|
||||
exec.status(), exec.startTime(), exec.endTime(),
|
||||
exec.durationMs() != null ? exec.durationMs() : 0L,
|
||||
exec.correlationId(), exec.exchangeId(),
|
||||
|
||||
@@ -27,7 +27,7 @@ public record ExecutionDetail(
|
||||
String executionId,
|
||||
String routeId,
|
||||
String agentId,
|
||||
String groupName,
|
||||
String applicationName,
|
||||
String status,
|
||||
Instant startTime,
|
||||
Instant endTime,
|
||||
|
||||
@@ -74,7 +74,7 @@ public class SearchIndexer implements SearchIndexerStats {
|
||||
.toList();
|
||||
|
||||
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.startTime(), exec.endTime(), exec.durationMs(),
|
||||
exec.errorMessage(), exec.errorStacktrace(), processorDocs));
|
||||
|
||||
@@ -38,18 +38,18 @@ public class IngestionService {
|
||||
this.bodySizeLimit = bodySizeLimit;
|
||||
}
|
||||
|
||||
public void ingestExecution(String agentId, String groupName, RouteExecution execution) {
|
||||
ExecutionRecord record = toExecutionRecord(agentId, groupName, execution);
|
||||
public void ingestExecution(String agentId, String applicationName, RouteExecution execution) {
|
||||
ExecutionRecord record = toExecutionRecord(agentId, applicationName, execution);
|
||||
executionStore.upsert(record);
|
||||
|
||||
if (execution.getProcessors() != null && !execution.getProcessors().isEmpty()) {
|
||||
List<ProcessorRecord> processors = flattenProcessors(
|
||||
execution.getProcessors(), record.executionId(),
|
||||
record.startTime(), groupName, execution.getRouteId(),
|
||||
record.startTime(), applicationName, execution.getRouteId(),
|
||||
null, 0);
|
||||
executionStore.upsertProcessors(
|
||||
record.executionId(), record.startTime(),
|
||||
groupName, execution.getRouteId(), processors);
|
||||
applicationName, execution.getRouteId(), processors);
|
||||
}
|
||||
|
||||
eventPublisher.accept(new ExecutionUpdatedEvent(
|
||||
@@ -72,13 +72,13 @@ public class IngestionService {
|
||||
return metricsBuffer;
|
||||
}
|
||||
|
||||
private ExecutionRecord toExecutionRecord(String agentId, String groupName,
|
||||
private ExecutionRecord toExecutionRecord(String agentId, String applicationName,
|
||||
RouteExecution exec) {
|
||||
String diagramHash = diagramStore
|
||||
.findContentHashForRoute(exec.getRouteId(), agentId)
|
||||
.orElse("");
|
||||
return new ExecutionRecord(
|
||||
exec.getExchangeId(), exec.getRouteId(), agentId, groupName,
|
||||
exec.getExchangeId(), exec.getRouteId(), agentId, applicationName,
|
||||
exec.getStatus() != null ? exec.getStatus().name() : "RUNNING",
|
||||
exec.getCorrelationId(), exec.getExchangeId(),
|
||||
exec.getStartTime(), exec.getEndTime(),
|
||||
@@ -90,13 +90,13 @@ public class IngestionService {
|
||||
|
||||
private List<ProcessorRecord> flattenProcessors(
|
||||
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) {
|
||||
List<ProcessorRecord> flat = new ArrayList<>();
|
||||
for (ProcessorExecution p : processors) {
|
||||
flat.add(new ProcessorRecord(
|
||||
executionId, p.getProcessorId(), p.getProcessorType(),
|
||||
p.getDiagramNodeId(), groupName, routeId,
|
||||
p.getDiagramNodeId(), applicationName, routeId,
|
||||
depth, parentProcessorId,
|
||||
p.getStatus() != null ? p.getStatus().name() : "RUNNING",
|
||||
p.getStartTime() != null ? p.getStartTime() : execStartTime,
|
||||
@@ -109,7 +109,7 @@ public class IngestionService {
|
||||
if (p.getChildren() != null) {
|
||||
flat.addAll(flattenProcessors(
|
||||
p.getChildren(), executionId, execStartTime,
|
||||
groupName, routeId, p.getProcessorId(), depth + 1));
|
||||
applicationName, routeId, p.getProcessorId(), depth + 1));
|
||||
}
|
||||
}
|
||||
return flat;
|
||||
|
||||
@@ -23,7 +23,7 @@ public record ExecutionSummary(
|
||||
String executionId,
|
||||
String routeId,
|
||||
String agentId,
|
||||
String groupName,
|
||||
String applicationName,
|
||||
String status,
|
||||
Instant startTime,
|
||||
Instant endTime,
|
||||
|
||||
@@ -22,7 +22,7 @@ import java.util.List;
|
||||
* @param routeId exact match on route_id
|
||||
* @param agentId exact match on agent_id
|
||||
* @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 offset pagination offset (0-based)
|
||||
* @param limit page size (default 50, max 500)
|
||||
@@ -43,7 +43,7 @@ public record SearchRequest(
|
||||
String routeId,
|
||||
String agentId,
|
||||
String processorType,
|
||||
String group,
|
||||
String application,
|
||||
List<String> agentIds,
|
||||
int offset,
|
||||
int limit,
|
||||
@@ -80,12 +80,12 @@ public record SearchRequest(
|
||||
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) {
|
||||
return new SearchRequest(
|
||||
status, timeFrom, timeTo, durationMin, durationMax, correlationId,
|
||||
text, textInBody, textInHeaders, textInErrors,
|
||||
routeId, agentId, processorType, group, resolvedAgentIds,
|
||||
routeId, agentId, processorType, application, resolvedAgentIds,
|
||||
offset, limit, sortField, sortDir
|
||||
);
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ public interface ExecutionStore {
|
||||
void upsert(ExecutionRecord execution);
|
||||
|
||||
void upsertProcessors(String executionId, Instant startTime,
|
||||
String groupName, String routeId,
|
||||
String applicationName, String routeId,
|
||||
List<ProcessorRecord> processors);
|
||||
|
||||
Optional<ExecutionRecord> findById(String executionId);
|
||||
@@ -17,7 +17,7 @@ public interface ExecutionStore {
|
||||
List<ProcessorRecord> findProcessors(String executionId);
|
||||
|
||||
record ExecutionRecord(
|
||||
String executionId, String routeId, String agentId, String groupName,
|
||||
String executionId, String routeId, String agentId, String applicationName,
|
||||
String status, String correlationId, String exchangeId,
|
||||
Instant startTime, Instant endTime, Long durationMs,
|
||||
String errorMessage, String errorStacktrace, String diagramContentHash
|
||||
@@ -25,7 +25,7 @@ public interface ExecutionStore {
|
||||
|
||||
record ProcessorRecord(
|
||||
String executionId, String processorId, String processorType,
|
||||
String diagramNodeId, String groupName, String routeId,
|
||||
String diagramNodeId, String applicationName, String routeId,
|
||||
int depth, String parentProcessorId, String status,
|
||||
Instant startTime, Instant endTime, Long durationMs,
|
||||
String errorMessage, String errorStacktrace,
|
||||
|
||||
@@ -12,7 +12,7 @@ public interface StatsStore {
|
||||
ExecutionStats stats(Instant from, Instant to);
|
||||
|
||||
// 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
|
||||
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);
|
||||
|
||||
// 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
|
||||
StatsTimeseries timeseriesForRoute(Instant from, Instant to, int bucketCount,
|
||||
|
||||
@@ -4,7 +4,7 @@ import java.time.Instant;
|
||||
import java.util.List;
|
||||
|
||||
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,
|
||||
Instant startTime, Instant endTime, Long durationMs,
|
||||
String errorMessage, String errorStacktrace,
|
||||
|
||||
591
dashboard-after-click.md
Normal 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]: 1–25 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]
|
||||
1204
docs/superpowers/plans/2026-03-17-rbac-crud-gaps.md
Normal file
2392
docs/superpowers/plans/2026-03-17-rbac-management.md
Normal file
142
docs/superpowers/specs/2026-03-17-rbac-crud-gaps-design.md
Normal 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)
|
||||
327
docs/superpowers/specs/2026-03-17-rbac-management-design.md
Normal 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 V1–V10 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 V1–V10 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
|
||||
261
docs/ui-mocks/camel-developer-review.md
Normal 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.
|
||||
1502
docs/ui-mocks/mock-design-expert.html
Normal file
1651
docs/ui-mocks/mock-operator-expert.html
Normal file
1565
docs/ui-mocks/mock-usability-expert.html
Normal file
1707
docs/ui-mocks/mock-v2-dark.html
Normal file
2076
docs/ui-mocks/mock-v2-light.html
Normal file
1490
docs/ui-mocks/mock-v3-agent-health.html
Normal file
1945
docs/ui-mocks/mock-v3-exchange-detail.html
Normal file
2177
docs/ui-mocks/mock-v3-metrics-dashboard.html
Normal file
2336
docs/ui-mocks/mock-v3-route-detail.html
Normal file
BIN
live-agents-v2.png
Normal file
|
After Width: | Height: | Size: 75 KiB |
BIN
live-agents.png
Normal file
|
After Width: | Height: | Size: 72 KiB |
BIN
live-dashboard-detail.png
Normal file
|
After Width: | Height: | Size: 218 KiB |
BIN
live-dashboard-panel-v2.png
Normal file
|
After Width: | Height: | Size: 223 KiB |
BIN
live-dashboard-panel-v3.png
Normal file
|
After Width: | Height: | Size: 150 KiB |
BIN
live-dashboard-panel.png
Normal file
|
After Width: | Height: | Size: 240 KiB |
BIN
live-dashboard-v2.png
Normal file
|
After Width: | Height: | Size: 184 KiB |
BIN
live-dashboard-v3.png
Normal file
|
After Width: | Height: | Size: 210 KiB |
BIN
live-dashboard.png
Normal file
|
After Width: | Height: | Size: 150 KiB |
BIN
live-routes-v2.png
Normal file
|
After Width: | Height: | Size: 104 KiB |
BIN
mock-agent-instance.png
Normal file
|
After Width: | Height: | Size: 165 KiB |
BIN
mock-agents.png
Normal file
|
After Width: | Height: | Size: 138 KiB |
BIN
mock-dashboard-v2.png
Normal file
|
After Width: | Height: | Size: 82 KiB |
BIN
mock-dashboard.png
Normal file
|
After Width: | Height: | Size: 84 KiB |
BIN
mock-routes.png
Normal file
|
After Width: | Height: | Size: 174 KiB |
@@ -6,10 +6,10 @@ export function useExecutionStats(
|
||||
timeFrom: string | undefined,
|
||||
timeTo: string | undefined,
|
||||
routeId?: string,
|
||||
group?: string,
|
||||
application?: string,
|
||||
) {
|
||||
return useQuery({
|
||||
queryKey: ['executions', 'stats', timeFrom, timeTo, routeId, group],
|
||||
queryKey: ['executions', 'stats', timeFrom, timeTo, routeId, application],
|
||||
queryFn: async () => {
|
||||
const { data, error } = await api.GET('/search/stats', {
|
||||
params: {
|
||||
@@ -17,7 +17,7 @@ export function useExecutionStats(
|
||||
from: timeFrom!,
|
||||
to: timeTo || undefined,
|
||||
routeId: routeId || undefined,
|
||||
group: group || undefined,
|
||||
application: application || undefined,
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -49,10 +49,10 @@ export function useStatsTimeseries(
|
||||
timeFrom: string | undefined,
|
||||
timeTo: string | undefined,
|
||||
routeId?: string,
|
||||
group?: string,
|
||||
application?: string,
|
||||
) {
|
||||
return useQuery({
|
||||
queryKey: ['executions', 'timeseries', timeFrom, timeTo, routeId, group],
|
||||
queryKey: ['executions', 'timeseries', timeFrom, timeTo, routeId, application],
|
||||
queryFn: async () => {
|
||||
const { data, error } = await api.GET('/search/stats/timeseries', {
|
||||
params: {
|
||||
@@ -61,7 +61,7 @@ export function useStatsTimeseries(
|
||||
to: timeTo || undefined,
|
||||
buckets: 24,
|
||||
routeId: routeId || undefined,
|
||||
group: group || undefined,
|
||||
application: application || undefined,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
10
ui/src/api/schema.d.ts
vendored
@@ -1079,7 +1079,7 @@ export interface components {
|
||||
routeId?: string;
|
||||
agentId?: string;
|
||||
processorType?: string;
|
||||
group?: string;
|
||||
application?: string;
|
||||
agentIds?: string[];
|
||||
/** Format: int32 */
|
||||
offset?: number;
|
||||
@@ -1092,7 +1092,7 @@ export interface components {
|
||||
executionId: string;
|
||||
routeId: string;
|
||||
agentId: string;
|
||||
groupName: string;
|
||||
applicationName: string;
|
||||
status: string;
|
||||
/** Format: date-time */
|
||||
startTime: string;
|
||||
@@ -1327,7 +1327,7 @@ export interface components {
|
||||
errorStackTrace: string;
|
||||
diagramContentHash: string;
|
||||
processors: components["schemas"]["ProcessorNode"][];
|
||||
groupName?: string;
|
||||
applicationName?: string;
|
||||
children?: components["schemas"]["ProcessorNode"][];
|
||||
};
|
||||
ProcessorNode: {
|
||||
@@ -2977,7 +2977,7 @@ export interface operations {
|
||||
from: string;
|
||||
to?: string;
|
||||
routeId?: string;
|
||||
group?: string;
|
||||
application?: string;
|
||||
};
|
||||
header?: never;
|
||||
path?: never;
|
||||
@@ -3003,7 +3003,7 @@ export interface operations {
|
||||
to?: string;
|
||||
buckets?: number;
|
||||
routeId?: string;
|
||||
group?: string;
|
||||
application?: string;
|
||||
};
|
||||
header?: never;
|
||||
path?: never;
|
||||
|
||||
@@ -36,7 +36,7 @@ export default function Dashboard() {
|
||||
const { data: searchResult } = useSearchExecutions({
|
||||
timeFrom, timeTo,
|
||||
routeId: routeId || undefined,
|
||||
group: appId || undefined,
|
||||
application: appId || undefined,
|
||||
offset: 0, limit: 50,
|
||||
}, true);
|
||||
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: '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: 'startTime', header: 'Started', sortable: true, render: (v) => <MonoText size="xs">{new Date(v as string).toISOString().replace('T', ' ').slice(0, 19)}</MonoText> },
|
||||
{
|
||||
|
||||
@@ -94,7 +94,7 @@ export default function ExchangeDetail() {
|
||||
<div>
|
||||
<Breadcrumb items={[
|
||||
{ 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) || '' },
|
||||
]} />
|
||||
|
||||
@@ -109,11 +109,11 @@ export default function ExchangeDetail() {
|
||||
<Badge label={detail.status} color={detail.status === 'COMPLETED' ? 'success' : 'error'} variant="filled" />
|
||||
</div>
|
||||
<div className={styles.exchangeRoute}>
|
||||
Route: <span className={styles.routeLink} onClick={() => navigate(`/apps/${detail.groupName}/${detail.routeId}`)}>{detail.routeId}</span>
|
||||
{detail.groupName && (
|
||||
Route: <span className={styles.routeLink} onClick={() => navigate(`/apps/${detail.applicationName}/${detail.routeId}`)}>{detail.routeId}</span>
|
||||
{detail.applicationName && (
|
||||
<>
|
||||
<span className={styles.headerDivider}>·</span>
|
||||
App: <MonoText size="xs">{detail.groupName}</MonoText>
|
||||
App: <MonoText size="xs">{detail.applicationName}</MonoText>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -51,7 +51,7 @@ export default function RouteDetail() {
|
||||
timeFrom,
|
||||
timeTo,
|
||||
routeId: routeId || undefined,
|
||||
group: appId || undefined,
|
||||
application: appId || undefined,
|
||||
offset: 0,
|
||||
limit: 50,
|
||||
});
|
||||
@@ -59,7 +59,7 @@ export default function RouteDetail() {
|
||||
timeFrom,
|
||||
timeTo,
|
||||
routeId: routeId || undefined,
|
||||
group: appId || undefined,
|
||||
application: appId || undefined,
|
||||
status: 'FAILED',
|
||||
offset: 0,
|
||||
limit: 200,
|
||||
|
||||
1
ui/tsconfig.app.tsbuildinfo
Normal 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"}
|
||||
1
ui/tsconfig.node.tsbuildinfo
Normal file
@@ -0,0 +1 @@
|
||||
{"root":["./vite.config.ts"],"version":"5.9.3"}
|
||||
1
ui/tsconfig.tsbuildinfo
Normal 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"}
|
||||