Comprehensive spec and 20-task plan to close all gaps between @cameleer/design-system v0.0.2 mocks and the current server UI. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
26 KiB
UI Mock Alignment Design
Date: 2026-03-23
Status: Reviewed
Scope: Close all gaps between @cameleer/design-system mocks and the cameleer3-server UI
Context
The @cameleer/design-system package (v0.0.2) contains fully realized mock pages demonstrating the target UX for the Cameleer3 monitoring platform. The current server UI was built as a first pass and has significant deviations from these mocks across every page. This spec defines the work to align them.
Out of scope:
- Business context columns (Order ID, Customer) — not applicable to current data model
- Application log streaming — agent does not send logs; placeholder only
1. Backend — New Endpoints
1a. Processor Stats Endpoint
GET /api/v1/routes/metrics/processors
Exposes per-processor statistics. The current stats_1m_processor continuous aggregate groups by (bucket, group_name, route_id, processor_type) and lacks processor_id. TimescaleDB continuous aggregates cannot be ALTERed to add GROUP BY columns.
Migration: Add V7__processor_stats_by_id.sql creating a new continuous aggregate stats_1m_processor_detail:
CREATE MATERIALIZED VIEW stats_1m_processor_detail
WITH (timescaledb.continuous) AS
SELECT
time_bucket('1 minute', start_time) AS bucket,
group_name,
route_id,
processor_id,
processor_type,
COUNT(*) AS total_count,
COUNT(*) FILTER (WHERE status = 'FAILED') AS failed_count,
SUM(duration_ms) AS duration_sum,
MAX(duration_ms) AS duration_max,
approx_percentile(0.99, percentile_agg(duration_ms)) AS p99_duration
FROM processor_executions
GROUP BY bucket, group_name, route_id, processor_id, processor_type;
Leave the original stats_1m_processor intact (used elsewhere).
Controller: Add new method in existing RouteMetricsController.java (shares /api/v1/routes/metrics base path) rather than a separate controller.
Query params:
routeId(required) — filter by routeappId(optional) — filter by applicationfrom/to(optional) — time window, defaults to last 24h
Response: List<ProcessorMetrics>
record ProcessorMetrics(
String processorId, // unique processor ID within the route
String processorType, // e.g. "to", "process", "choice"
String routeId,
String appId,
long totalCount,
long failedCount,
double avgDurationMs,
double p99DurationMs,
double errorRate // failedCount / totalCount
)
Security: VIEWER+ role. Already covered by existing GET /api/v1/routes/** wildcard in SecurityConfig.
1b. Agent Metrics Query Endpoint
GET /api/v1/agents/{agentId}/metrics
Queries the agent_metrics hypertable and returns time-bucketed series.
Query params:
names(required) — comma-separated metric names (e.g.jvm.cpu.process,jvm.memory.heap.used)from/to(optional) — time window, defaults to last 1hbuckets(optional, default 60) — number of time buckets
Response: AgentMetricsResponse
record AgentMetricsResponse(
Map<String, List<MetricBucket>> metrics
)
record MetricBucket(
Instant time,
double value // avg within bucket
)
Implementation: Use time_bucket() on agent_metrics.collected_at, grouped by metric_name, averaged by metric_value. Filter by agent_id and optional tags if needed.
Security: VIEWER+ role. Requires new SecurityConfig rule: GET /api/v1/agents/*/metrics (existing /api/v1/agents rule is exact-match only, does not cover sub-paths).
1c. Enrich AgentInstanceResponse
Add fields to existing AgentInstanceResponse:
// existing fields...
String version, // from AgentInfo.version in registry
Map<String, Object> capabilities // from AgentInfo.capabilities
These values are already stored in the AgentRegistry's AgentInfo objects. The AgentRegistrationController.listAgents() method just needs to map them into the response DTO.
1d. Password Reset Endpoint
POST /api/v1/admin/users/{userId}/password
The current UpdateUserRequest has no password field. Add a dedicated endpoint for admin password reset.
Request body: SetPasswordRequest
record SetPasswordRequest(
@NotBlank String password
)
Response: 204 No Content
Implementation: Hash password with same BCrypt encoder used in createUser, update users.password_hash column.
Security: ADMIN role required (same as other user management endpoints).
New files: cameleer3-server-app/.../dto/SetPasswordRequest.java; new method in UserAdminController.
2. Dashboard Enhancements
2a. DetailPanel — Errors Section
When the selected execution has a non-null errorMessage:
- Insert an "Errors" section between Overview and Processors in the DetailPanel
- Display:
- Error class: parsed from
errorMessage(text before:or first line) - Error message: remainder of
errorMessage - Stack trace:
errorStackTracein a collapsibleCodeBlock(content prop)
- Error class: parsed from
- Use
Alertvariant="error" for the error class/message,Collapsible+CodeBlockfor the stack trace
2b. DetailPanel — Route Flow Tab
Add a third tab to the DetailPanel tabs: Overview | Processors | Route Flow
- Fetch diagram via
useDiagramByRoute(execution.groupName, execution.routeId) - Render
RouteFlowcomponent from design system - Overlay execution data: map each
ProcessorNodestatus onto diagram nodes usingdiagramNodeId - Color nodes by status: success (green), failed (red), running (blue)
- Show duration labels on nodes
- RouteFlow overlay API: The
RouteFlowcomponent accepts execution data to color nodes. During implementation, read theRouteFlow.tsxsource in the design system to confirm the exact props interface (likely anoverlaysornodeStatesprop mapping node IDs to status/duration). MapProcessorNode.diagramNodeId→PositionedNode.idto connect execution data to diagram nodes.
2c. Stat Card Alignment
Change the 5 stat cards to match mock semantics:
| Position | Label | Value | Source |
|---|---|---|---|
| 1 | Throughput | exchanges/s | totalCount / timeWindowSeconds from stats |
| 2 | Error Rate | % | failedCount / totalCount * 100 from stats |
| 3 | Avg Latency | ms | avgDurationMs from stats |
| 4 | P99 Latency | ms | p99LatencyMs from stats |
| 5 | In-Flight | count | activeCount from stats |
Each card includes:
Sparklinefrom timeseries buckets (existing)- Trend arrow: compare current vs
prev*fields, show up/down indicator
3. Exchange Detail Enhancements
3a. Correlation Chain
Below the exchange header card, add a "Correlation Chain" section:
- Data source:
POST /search/executionswith filter{ correlationId: execution.correlationId } - Rendering: Horizontal chain of small cards connected by arrows
- Each card: route name,
StatusDot, duration, relative timestamp - Current exchange highlighted
- Click navigates to that exchange (
/exchanges/:id)
- Each card: route name,
- Conditional: Only show section when correlationId is present and search returns > 1 result
- Limit: Request with
limit: 20to prevent excessive results. If more exist, show "+N more" link - Hook:
useCorrelationChain(correlationId)— new query hook wrapping the search call
3b. Timeline / Flow Toggle
Above the processor timeline section:
- Add
SegmentedTabswith options: Timeline | Flow - Timeline (default): existing
ProcessorTimelinecomponent (Gantt view) - Flow:
RouteFlowcomponent with execution overlay- Fetch diagram via
useDiagramByRoute(execution.groupName, execution.routeId)(same as 2b) - Color nodes by processor status, show duration labels
- Clicking a processor node in either view selects it and loads its snapshot
- Fetch diagram via
3c. Header Enrichment
Add to the exchange header:
- Processor count:
execution.processors.length(or recursive count for nested trees) - Display as a stat in the header's right section alongside duration
4. Route Detail Page (NEW)
New page at /routes/:appId/:routeId. Currently this path renders a filtered RoutesMetrics; replace with a dedicated route detail page. The filtered table/chart view from RoutesMetrics is not lost — it is subsumed by the Performance and Recent Executions tabs in the new page, which provide the same data in a richer context alongside the route diagram.
Update router.tsx: the /routes/:appId/:routeId route imports a new RouteDetail component instead of RoutesMetrics. The /routes and /routes/:appId routes remain unchanged (continue to render RoutesMetrics).
4a. Route Header Card
Card displaying:
- Route name (
routeId) and application name (appId) - Health status from route catalog (
useRouteCatalog()filtered) - Exchange count (last 24h)
- Last seen timestamp
- Back link to
/routes/:appId
4b. Route Diagram + Processor Stats (Side-by-Side)
Two-column grid:
- Left:
RouteFlowcomponent rendering the route diagram- Data from
useDiagramByRoute(appId, routeId)oruseDiagramLayout(contentHash)
- Data from
- Right: Processor stats table from new endpoint (1a)
DataTablecolumns: Processor ID, Type, Executions, Avg Duration, P99 Duration, Error Rate- Data from
useProcessorMetrics(routeId, appId)
4c. Tabbed Section
Tabs component with three tabs:
Performance tab:
- 2x2 chart grid (same pattern as RoutesMetrics) filtered to this specific route
- Data from
useStatsTimeseries(from, to, routeId, appId)
Recent Executions tab:
DataTableshowing recent executions for this route- Data from
useSearchExecutions({ routeId, group: appId, limit: 20, sortField: 'startTime', sortDir: 'desc' }) - Columns: Status, Execution ID, Duration, Start Time, Error Message
- Row click navigates to
/exchanges/:id
Error Patterns tab:
- Group failed executions by
errorMessage - Display: error message (truncated), count, last occurrence timestamp, link to sample execution
- Data from
useSearchExecutions({ routeId, group: appId, status: 'FAILED', limit: 100 })— client-side grouping byerrorMessage
4d. CSS Module
RouteDetail.module.css with classes:
.headerCard— surface card, padding, margin-bottom.diagramStatsGrid— 2-column grid.diagramPane/.statsPane— surface cards.tabSection— margin-top.chartGrid— 2x2 grid (reuse pattern from RoutesMetrics)
5. Agent Health Enhancements
5a. DetailPanel (Slide-In)
Add a DetailPanel from the design system, triggered by clicking an instance row in a GroupCard.
Overview tab:
- Status with
StatusDotandBadge - Application name, version (from enriched AgentInstanceResponse, section 1c)
- Uptime (formatted), last heartbeat (relative time)
- TPS, error rate
- Active routes / total routes
- Memory usage:
ProgressBar— data fromGET /agents/{id}/metrics?names=jvm.memory.heap.used,jvm.memory.heap.max&buckets=1(single latest bucket gives the most recent averaged value) - CPU usage:
ProgressBar— data fromGET /agents/{id}/metrics?names=jvm.cpu.process&buckets=1(single latest bucket)
Performance tab:
- Two
LineChartcomponents:- Throughput over time (from timeseries stats filtered by agentId, or from agent metrics)
- Error rate over time
5b. Instance Table Enrichment
Add columns to the instance rows within each GroupCard:
| Column | Source |
|---|---|
| Status dot | agent.status (existing) |
| Instance name | agent.name (existing) |
| State badge | agent.status (existing) |
| Uptime | agent.uptimeSeconds → formatted "2d 4h" / "15m" |
| TPS | agent.tps (existing) |
| Error rate | agent.errorRate → percentage |
| Last heartbeat | agent.lastHeartbeat → relative "2m ago" |
| Link | Icon button → /agents/:appId/:instanceId |
5c. Alert Banners
When a GroupCard contains instances with status DEAD:
- Show
Alertvariant="error" at the top of the card body - Message:
"N instance(s) unreachable"where N is the count of DEAD instances
5d. Stat Card Alignment
Replace current 4 cards (Total, Live, Stale, Dead) with 5 cards matching mock:
| Label | Value | Accent |
|---|---|---|
| Total Agents | count (subtitle: "N live / N stale / N dead") | default |
| Applications | count of unique appIds | default |
| Active Routes | sum of activeRoutes across live agents | default |
| Total TPS | sum of tps across live agents | default |
| Dead | count of dead agents | error |
5e. Scope Trail
Add breadcrumb below stat cards:
- All agents view:
Agentswith liveBadgeshowing "N live" - Filtered by app:
Agents>{appName}with healthBadge(live/stale/dead color)
6. Agent Instance Enhancements
6a. JVM Metrics Charts (3x2 Grid)
Replace current 2-column chart grid with 3x2 grid. All data from new endpoint (1b).
| Chart | Type | Metric Name(s) |
|---|---|---|
| CPU Usage | AreaChart | jvm.cpu.process (0-1 scale, display as %) |
| Memory (Heap) | AreaChart | jvm.memory.heap.used + jvm.memory.heap.max (two series) |
| Throughput | AreaChart | from useStatsTimeseries filtered by agent (existing) |
| Error Rate | LineChart | from useStatsTimeseries filtered by agent (existing) |
| Thread Count | LineChart | jvm.threads.count |
| GC Pauses | BarChart | jvm.gc.time |
Hook: useAgentMetrics(agentId, metricNames[], from, to, buckets) — wraps endpoint 1b.
6b. Process Information Card
Card with key-value pairs:
| Key | Source |
|---|---|
| JVM Version | agent.capabilities.jvmVersion or parse from registration |
| Camel Version | agent.capabilities.camelVersion |
| Spring Boot | agent.capabilities.springBootVersion |
| Started | agent.registeredAt formatted |
| Capabilities | render as tags: tracing, metrics, diagrams, replay |
Data from enriched AgentInstanceResponse (section 1c). If version details aren't in current capabilities, they can be added to agent registration in a future iteration — show what's available.
6c. Stat Card Alignment
Replace current 4 cards with 5 cards matching mock:
| Label | Value | Source |
|---|---|---|
| CPU | % | latest jvm.cpu.process from agent metrics |
| Memory | % | latest heap.used / heap.max * 100 |
| Throughput | req/s | agent.tps |
| Errors | % | agent.errorRate |
| Uptime | formatted | agent.uptimeSeconds |
CPU and Memory require a small fetch from endpoint 1b (latest single value).
6d. Application Log Placeholder
Below the EventFeed card, add an EmptyState component:
- Title: "Application Logs"
- Description: "Application log streaming is not yet available"
- No action button
6e. Version Badge in Scope Trail
Breadcrumb: Agents > {appName} > {instanceName}
- Add
Badgenext to instance name showing version (from enriched response) - Add
StatusDot+ statusBadgefor visual state
7. Admin & Miscellaneous
7a. OIDC Config — Default Roles
Add a "Default Roles" section to the OIDC config page:
- Display current default roles as
Tagcomponents (removable, click X to remove) Input+ "Add"Buttonto add a role- Validate against existing roles from
useRoles()query - Persist via existing OIDC config save endpoint
7b. OIDC Config — ConfirmDialog on Delete
Replace direct delete button with ConfirmDialog:
- Message: "Delete OIDC configuration? All OIDC users will lose access."
- Require typing "DELETE" to confirm
7c. Design System Update
Update @cameleer/design-system from ^0.0.1 to ^0.0.2 in ui/package.json.
TopBar onLogout prop: Replace the custom Dropdown + Avatar logout hack in LayoutShell.tsx with the TopBar's new onLogout prop:
<TopBar
breadcrumb={breadcrumb}
user={{ name: username }}
onLogout={handleLogout}
/>
Remove the manual Avatar/Dropdown logout code.
Verification needed during implementation: Confirm that the TopBar v0.0.2 renders a user avatar/menu internally when user + onLogout are provided. If it only renders a bare logout button without the "Signed in as" display, keep the custom Avatar/Dropdown and just wire up the TopBar's onLogout as an additional trigger.
7d. Regenerate schema.d.ts
After backend endpoints are added, regenerate types from the running server:
npm run generate-api:live
This ensures all new DTOs (ProcessorMetrics, AgentMetricsResponse, MetricBucket, enriched AgentInstanceResponse) are accurately typed.
8. RBAC / User Management Overhaul
The current RBAC page is a basic DataTable + Modal CRUD interface. The design system mock implements a split-pane detail-oriented admin panel with rich interactions. This section describes the full rebuild.
8a. Layout — Split-Pane Replaces DataTable
All three tabs (Users, Groups, Roles) adopt the same layout pattern:
┌─────────────────────┬────────────────────┐
│ List Pane (52%) │ Detail Pane (48%) │
│ │ │
│ [Search input] │ [Selected entity │
│ [+ Create button] │ detail view] │
│ │ │
│ [Inline create form]│ │
│ │ │
│ [Scrollable entity │ │
│ list with avatars, │ │
│ badges, tags] │ │
│ │ │
└─────────────────────┴────────────────────┘
New file: ui/src/pages/Admin/UserManagement.module.css
.splitPane— CSS grid52fr 48fr, full height.listPane— scrollable, border-right.detailPane— scrollable, padding.entityItem/.entityItemSelected— list items with hover/selected states.entityInfo,.entityName,.entityMeta,.entityTags— list item layout.createForm,.createFormActions— inline form styling.metaGrid— key-value metadata layout.sectionTags— tag group with wrap.inheritedNote— small italic annotation text.securitySection/.resetForm— password management styling
Keep existing stat cards above tabs — these are a useful addition not present in the mock.
8b. Users Tab
List pane:
- Search:
Inputwith search icon, filters across username, displayName, email (client-side) - Create button: "+ Add User" opens inline form (not modal)
- Inline create form:
Input: Username (required), Display Name, EmailInput: Password (required)- Client-side validation: duplicate username check, required fields
- Cancel + Create buttons
- Note: Admin-created users are always local. OIDC users are auto-provisioned on first login (no admin creation needed). The create form does not include a provider selector.
- Entity list:
role="listbox", each itemrole="option"withtabIndex={0}Avatar(initials, size sm)- Display name + provider
Badge(if not local) - Email + group path in meta line
- Direct roles and groups as small
Badgetags - Click or Enter/Space to select → populates detail pane
Detail pane (when user selected):
- Header:
Avatar(lg) + Display name (InlineEditfor rename) + Email + Delete button - Status: "Active"
Tag - Metadata grid: User ID (
MonoText), Created (formatted date+time), Provider - Security section:
- Local users: masked password display + "Reset password" button → toggles inline form (new password
Input+ Cancel/Set) - OIDC users:
InfoCallout"Password managed by identity provider"
- Local users: masked password display + "Reset password" button → toggles inline form (new password
- Group membership:
- Current groups as removable
Tagcomponents MultiSelectdropdown to add groups- Warning on removal if inherited roles would be revoked
- Current groups as removable
- Effective roles:
- Direct roles: removable
Tag(warning color) - Inherited roles: dashed
Badgewith "↑ groupName" source notation (opacity 0.65, non-removable) MultiSelectto add direct roles- Note: "Roles with ↑ are inherited through group membership"
- Direct roles: removable
- Delete:
ConfirmDialogrequiring username to be typed. Self-delete guard (can't delete own account).
API hooks used: useUsers, useUser, useCreateUser, useUpdateUser, useDeleteUser, useAssignRoleToUser, useRemoveRoleFromUser, useAddUserToGroup, useRemoveUserFromGroup, useGroups, useRoles
8c. Groups Tab
List pane:
- Search: filter by group name
- Create form: inline with name + parent group
Selectdropdown (options: "Top-level" + all existing groups) - Entity list:
Avatar+ group name- Meta: "Child of {parent}" or "Top-level" + child count + member count
- Role tags
Detail pane:
- Header: Group name (
InlineEditfor non-built-in) + parent info + Delete button (disabled for built-in Admins group) - Metadata: Group ID (
MonoText) - Parent group: display current parent
- Members: removable
Taglist +MultiSelectto add users. Note: "+ all members of child groups" if applicable - Child groups: removable
Taglist +MultiSelectto add existing groups as children. Circular reference prevention (can't add ancestor as child) - Assigned roles: removable
Taglist +MultiSelectto add roles. Warning on removal: "Removing {role} from {group} will affect N member(s). Continue?" - Delete:
ConfirmDialog. Guard: built-in Admins group cannot be deleted.
API hooks used: useGroups, useGroup, useCreateGroup, useUpdateGroup, useDeleteGroup, useAssignRoleToGroup, useRemoveRoleFromGroup
8d. Roles Tab
List pane:
- Search: filter by role name
- Create form: inline with name (auto-uppercase) + description
- Entity list:
Avatar+ role name + "system"Badge(if system role)- Meta: description + assignment count
- Tags: assigned groups (success color) + direct users
Detail pane:
- Header: Role name + description + Delete button (disabled for system roles)
- Metadata: Role ID (
MonoText), scope, type (system/custom — "System role (read-only)") - Assigned to groups: view-only
Taglist (shows which groups have this role) - Assigned to users (direct): view-only
Taglist - Effective principals: filled
Badge(direct assignment) + dashedBadge(inherited via group). Note: "Dashed entries inherit this role through group membership"
API hooks used: useRoles, useRole, useCreateRole, useUpdateRole, useDeleteRole
8e. Shared Patterns
- Toast notifications for all mutations (create, update, delete, assign, remove) — use
useToastfrom design system - Cascade warnings when actions affect other entities (removing role from group, removing user from group with roles)
- Keyboard accessibility: Enter/Space to select, ARIA roles (
listbox,option),aria-selected - Mutation button states: disable while in-flight, show spinner
- ToastProvider: Add
ToastProviderfrom design system toLayoutShell.tsx(or app root inmain.tsx) to enableuseToast()hook across admin pages - Graceful empty states: When agent metrics are unavailable (agent not sending a particular metric), show per-chart empty state rather than crashing. Check metric name existence in response before rendering.
File Impact Summary
New files:
ui/src/pages/Routes/RouteDetail.tsx+RouteDetail.module.cssui/src/pages/Admin/UserManagement.module.cssui/src/pages/Admin/UsersTab.tsxui/src/pages/Admin/GroupsTab.tsxui/src/pages/Admin/RolesTab.tsxui/src/api/queries/agent-metrics.ts(useAgentMetrics hook)ui/src/api/queries/processor-metrics.ts(useProcessorMetrics hook)ui/src/api/queries/correlation.ts(useCorrelationChain hook)cameleer3-server-app/.../controller/AgentMetricsController.javacameleer3-server-app/.../dto/ProcessorMetrics.javacameleer3-server-app/.../dto/AgentMetricsResponse.javacameleer3-server-app/.../dto/MetricBucket.javacameleer3-server-app/.../dto/SetPasswordRequest.javacameleer3-server-app/src/main/resources/db/migration/V7__processor_stats_by_id.sql
Modified files:
ui/package.json— design system^0.0.2ui/src/router.tsx— add RouteDetail routeui/src/components/LayoutShell.tsx— TopBaronLogoutprop, remove Dropdown/Avatarui/src/pages/Dashboard/Dashboard.tsx— error section, RouteFlow tab, stat card changesui/src/pages/Dashboard/Dashboard.module.css— new classesui/src/pages/ExchangeDetail/ExchangeDetail.tsx— correlation chain, flow toggle, processor countui/src/pages/ExchangeDetail/ExchangeDetail.module.css— new classesui/src/pages/Routes/RoutesMetrics.tsx— stat card adjustmentsui/src/pages/AgentHealth/AgentHealth.tsx— DetailPanel, table enrichment, alert banners, stat cards, scope trailui/src/pages/AgentHealth/AgentHealth.module.css— new classesui/src/pages/AgentInstance/AgentInstance.tsx— 3x2 charts, process info, stat cards, log placeholder, version badgeui/src/pages/AgentInstance/AgentInstance.module.css— new classesui/src/pages/Admin/RbacPage.tsx— restructured to container with split-pane tabsui/src/pages/Admin/OidcConfigPage.tsx— default roles, ConfirmDialogui/src/api/schema.d.ts— regenerated with new typescameleer3-server-app/.../dto/AgentInstanceResponse.java— add version, capabilitiescameleer3-server-app/.../controller/AgentRegistrationController.java— map version/capabilitiescameleer3-server-app/.../controller/RouteMetricsController.java— add processor stats methodcameleer3-server-app/.../controller/UserAdminController.java— add password reset methodcameleer3-server-app/.../SecurityConfig.java— add rule forGET /api/v1/agents/*/metricsui/src/main.tsxorui/src/components/LayoutShell.tsx— addToastProvidercameleer3-server-app/.../OpenApiConfig.java— register new DTOs
Backend migration:
V7__processor_stats_by_id.sql— newstats_1m_processor_detailcontinuous aggregate withprocessor_idgrouping