> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Fix critical UI bugs, standardize layout/interaction patterns across all pages, improve contrast/readability, and polish data formatting and admin UX.
**Architecture:** All changes are in the existing UI codebase (`ui/src/`) and one backend controller. No new components or architecture changes — this is about adopting existing design system patterns consistently and fixing bugs. The design system package `@cameleer/design-system` provides shared CSS modules and components that many pages don't yet use.
- [ ]**Step 1: Add deployments redirect in router.tsx**
In `ui/src/router.tsx`, add a redirect route for `/server/deployments`. Find the existing legacy redirects (around lines 63-67 where `logs` redirects to `/runtime` and `config` redirects to `/apps`). Add a `deployments` redirect in the same block:
This matches the pattern used by CPU (line 95), Heap (line 101), Threads (line 107), Throughput (line 120), and Error Rate (line 127) — all use `x: i`.
- [ ]**Step 3: Verify visually**
Open the app in a browser:
1. Navigate to `https://<host>/server/deployments` — should redirect to `/server/apps`
2. Navigate to Runtime > click an agent > scroll to GC Pauses chart — X-axis should show numeric labels, not ISO timestamps
- [ ]**Step 2: Fix backend — return error body when OIDC enabled**
In `UserAdminController.java`, find the OIDC check (around line 92-93):
```java
// BEFORE:
if (oidcEnabled) {
return ResponseEntity.badRequest().build();
}
```
Change to return a descriptive error:
```java
// AFTER:
if (oidcEnabled) {
return ResponseEntity.badRequest()
.body(java.util.Map.of("error", "Local user creation is disabled when OIDC is enabled. Users are provisioned automatically via SSO."));
}
```
- [ ]**Step 3: Fix frontend — hide create form when OIDC local creation would fail**
In `ui/src/pages/Admin/UsersTab.tsx`, the `+ Add user` button is on the `EntityList` component (around line 318). The OIDC config state needs to be checked. Find where OIDC state is available (it may already be fetched for the Local/OIDC radio toggle).
Add a check: when the OIDC provider is enabled AND the user tries to create a `local` user, show a message instead of the form. The simplest approach: keep the form but when `newProvider === 'local'` and OIDC is the only provider, show an info callout explaining local creation is disabled. The existing `InfoCallout` for OIDC users (lines 247-251) provides the pattern.
After the password field (around line 246), add a check for the case where local creation fails:
```tsx
{newProvider === 'local' && oidcEnabled && (
<InfoCallout variant="amber">
Local user creation is disabled while OIDC is enabled.
Switch to OIDC to pre-register a user, or disable OIDC first.
</InfoCallout>
)}
```
Also update the error toast handler (in `handleCreate`) to surface the API error message. Find the catch block and use the response body:
- Investigate: `ui/src/hooks/` (any SSE or polling hooks)
- [ ]**Step 1: Identify the navigation trigger**
The audit found that admin pages sporadically redirect to `/server/exchanges`. LayoutShell.tsx has a path normalization at line 444-449:
```typescript
const effectiveSelectedPath = useMemo(() => {
const raw = sidebarRevealPath ?? location.pathname;
const match = raw.match(/^\/(exchanges|dashboard|apps|runtime)\/([^/]+)(\/.*)?$/);
if (match) return `/exchanges/${match[2]}${match[3] ?? ''}`;
return raw;
}, [sidebarRevealPath, location.pathname]);
```
This rewrites ALL tab paths to `/exchanges/...` for sidebar highlighting. But this is a `useMemo`, not a navigation call. Search for:
1. Any `useNavigate()` or `navigate()` calls triggered by data updates
2. Any `useEffect` that calls `navigate` based on exchange/catalog data changes
3. Any auto-refresh callback that might trigger navigation
4. The `sidebarRevealPath` state — what sets it?
```bash
cd ui/src && grep -rn "navigate(" components/LayoutShell.tsx | head -20
cd ui/src && grep -rn "sidebarRevealPath" components/LayoutShell.tsx | head -10
```
- [ ]**Step 2: Apply fix based on investigation**
The exact fix depends on what Step 1 reveals. The principle: SSE/polling data updates must NEVER trigger `navigate()` when the user is on an admin page. Common patterns to look for:
- A `useEffect` that watches exchange data and navigates to show the latest exchange
- A sidebar tree item click handler that fires on data refresh (re-render causes focus/activation)
- An auto-refresh timer that resets the route
If it's a `useEffect` with `navigate`, add a route guard:
```typescript
// Only navigate if we're already on the exchanges tab
if (!location.pathname.startsWith('/server/exchanges')) return;
```
If it's a sidebar focus issue, prevent navigation on programmatic focus:
```typescript
// Only navigate on explicit user clicks, not focus events
onClick={(e) => { if (e.isTrusted) navigate(path); }}
```
- [ ]**Step 3: Verify by navigating admin pages while data is flowing**
1. Open Admin > Users & Roles
2. Wait 30-60 seconds while agents are sending data
3. Interact with the form (click fields, open dropdowns)
4. Confirm no redirect to /server/exchanges occurs
- [ ]**Step 4: Commit**
```bash
git add ui/src/components/LayoutShell.tsx # and any other modified files
git commit -m "fix: prevent SSE data updates from triggering navigation on admin pages"
```
---
## Task 4: Exchanges table containment and Dashboard padding
<span className={tableStyles.tableMeta}>{exchanges.length} of {formatNumber(total)} exchanges</span>
{/* existing auto-refresh indicator */}
</div>
</div>
<DataTable ... />
</div>
```
Replace the custom `.tableHeader`, `.tableTitle`, `.tableRight`, `.tableMeta` class usages with the shared module equivalents.
- [ ]**Step 2: Remove custom table classes from Dashboard.module.css**
In `ui/src/pages/Dashboard/Dashboard.module.css`, remove the custom `.tableHeader`, `.tableTitle`, `.tableRight`, `.tableMeta` classes (they're now provided by the shared module). Keep any other custom classes that aren't table-related.
- [ ]**Step 3: Add side padding to DashboardTab**
In `ui/src/pages/DashboardTab/DashboardTab.module.css`, update `.content`:
Find each configuration group in the `ConfigSubTab` component (around lines 722-860). Each logical section (Monitoring settings, Resources, Variables, etc.) should be wrapped in a section card:
```tsx
<div className={sectionStyles.section}>
<SectionHeader>Monitoring</SectionHeader>
{/* existing monitoring controls: Engine Level, Payload Capture, etc. */}
</div>
```
Apply this to each sub-tab's content area. The existing `SectionHeader` components mark where sections begin — wrap each section header + its controls in `sectionStyles.section`.
- [ ]**Step 3: Replace manual `<table>` with DataTable in OverviewSubTab**
Find the manual `<table>` in `OverviewSubTab` (lines 623-680). Replace with:
- [ ]**Step 1: Replace duplicated card CSS in DashboardTab**
In `DashboardTab.module.css`, the `.errorsSection` and `.diagramSection` classes duplicate the card pattern. In the TSX files that use them, replace with the shared module:
```typescript
import tableStyles from '../../styles/table-section.module.css';
```
Replace `className={styles.errorsSection}` with `className={tableStyles.tableSection}` (since these are table-like sections). Remove `.errorsSection` and `.diagramSection` from `DashboardTab.module.css`. Keep any non-card properties (like `height: 280px` on diagramSection) as a separate class composed with the shared one:
- [ ]**Step 2: Replace duplicated card CSS in AgentHealth**
In `AgentHealth.module.css`, remove the card pattern from `.configBar` and `.eventCard`. Import `sectionStyles`:
```typescript
import sectionStyles from '../../styles/section-card.module.css';
```
Replace `className={styles.configBar}` with `className={sectionStyles.section}` (keep any custom padding/margin in a composed class if needed). Same for `.eventCard`.
- [ ]**Step 3: Replace duplicated card CSS in AgentInstance**
Same pattern for `.processCard` and `.timelineCard` in `AgentInstance.module.css`. Import `sectionStyles` and replace.
- [ ]**Step 4: Replace duplicated card CSS in ClickHouseAdminPage**
Replace `.pipelineCard` with `sectionStyles.section`.
- [ ]**Step 5: Wrap Database admin tables in tableSection**
In `DatabaseAdminPage.tsx`, import `tableStyles` and wrap each `DataTable` in a `tableStyles.tableSection` div with a `tableStyles.tableHeader`.
- [ ]**Step 6: Wrap RBAC and Environments detail sections in section cards**
In `UsersTab.tsx`, `GroupsTab.tsx`, and `EnvironmentsPage.tsx`, import `sectionStyles` and wrap detail panel sections in `sectionStyles.section`. Each section header + its content becomes a card:
```tsx
import sectionStyles from '../../styles/section-card.module.css';
// In the detail panel:
<div className={sectionStyles.section}>
<SectionHeader>Group Membership</SectionHeader>
{/* existing membership tags */}
</div>
<div className={sectionStyles.section}>
<SectionHeader>Effective Roles</SectionHeader>
{/* existing role tags */}
</div>
```
- [ ]**Step 7: Verify visually**
1. Dashboard: errors section and diagram section should still look the same (card styling from shared module now)
2. Runtime > Agent detail: process card and timeline card should have consistent card styling
3. Admin > Database: tables should have card wrappers
4. Admin > Users & Roles: detail panel sections should have card backgrounds
5. Admin > Environments: detail panel sections should have card backgrounds
In `ui/src/pages/Routes/RouteDetail.tsx`, find the tap modal footer Cancel button and change `variant="secondary"` to `variant="ghost"`.
- [ ]**Step 7: Add loading prop to ConfirmDialogs that lack it**
In `ui/src/pages/Admin/OidcConfigPage.tsx` (around line 258), find the ConfirmDialog for OIDC delete. Add loading prop — track the delete operation state:
```tsx
<ConfirmDialog
...existing props...
loading={deleting} // add this prop
/>
```
In `ui/src/pages/Routes/RouteDetail.tsx` (around line 992), find the tap delete ConfirmDialog. Add `loading` prop if a mutation state is available.
- [ ]**Step 8: Verify**
1. AppConfigDetailPage: Edit mode shows Cancel (left) | Save (right, primary, with spinner)
Make all form fields read-only when `!editing`. The form should display values from `form` in read mode and `formDraft` in edit mode. Update `handleSave` to use `formDraft` and call `cancelEditing` on success (after updating `form` with saved data).
Remove the inline `<Alert>` (line 138-139) — keep only the toast for errors (fixes 2b.14).
- [ ]**Step 2: Replace Spinner with PageLoader across admin pages**
In each of these files, replace bare `<Spinner>` returns with `<PageLoader />`:
For each match, change to `font-size: 12px`. This includes StatCard labels, overview labels, table meta, sidebar tree labels, chart titles, pagination text, etc.
- [ ]**Step 6: Verify visually**
1. Check light mode: muted text should be noticeably darker
2. Check dark mode: muted text should be clearly readable, faint text should be visible
3. All labels and meta text should be at least 12px
4. Run a contrast checker browser extension to verify ratios
- [ ]**Step 7: Commit**
```bash
git add -A ui/src/
git commit -m "fix: WCAG AA contrast compliance for --text-muted/--text-faint, 12px font floor"
```
---
## Task 10: Duration formatter and exchange ID truncation
**Spec items:** 4.1, 4.5
**Files:**
- Modify: `ui/src/utils/format-utils.ts`
- Modify: `ui/src/pages/Dashboard/Dashboard.tsx`
- [ ]**Step 1: Improve the shared duration formatter**
In `ui/src/utils/format-utils.ts`, find `formatDuration` (around line 1-5):
```typescript
// BEFORE:
export function formatDuration(ms: number): string {
if (ms >= 60_000) return `${(ms / 1000).toFixed(0)}s`;
if (ms >= 1000) return `${(ms / 1000).toFixed(2)}s`;
return `${ms}ms`;
}
```
Replace with:
```typescript
// AFTER:
export function formatDuration(ms: number): string {
## Task 11: Attributes column, status terminology, and agent names
**Spec items:** 4.2, 4.3, 4.4
**Files:**
- Modify: `ui/src/pages/Dashboard/Dashboard.tsx`
- Modify: `ui/src/utils/format-utils.ts` (if `statusLabel` is defined there)
- [ ]**Step 1: Hide Attributes column when empty**
In `ui/src/pages/Dashboard/Dashboard.tsx`, find the Attributes column definition (around line 96-108). Make it conditional based on whether any row has attributes:
Find `statusLabel` in `ui/src/utils/format-utils.ts` (or wherever it's defined). It should map:
```typescript
export function statusLabel(status: string): string {
switch (status) {
case 'COMPLETED': return 'OK';
case 'FAILED': return 'ERR';
case 'RUNNING': return 'RUN';
default: return status;
}
}
```
Verify this function is used in BOTH the exchange table AND the exchange detail panel. If the detail panel uses raw `status` instead of `statusLabel()`, update it to use the same function.
Search for where the detail panel displays status:
In `ui/src/pages/AgentInstance/AgentInstance.tsx`, check how the chart components accept Y-axis configuration. The `LineChart` and `BarChart` components likely accept a `yMax` or `yDomain` prop.
For the Throughput chart (around line 339), if it uses a fixed max, remove it or set it dynamically:
```typescript
// If the chart has a yMax prop, compute it from data:
For the Memory chart (around line 325), add a reference line at max heap. Check if the chart component supports a `referenceLine` or `threshold` prop:
```typescript
<LineChart
series={heapSeries}
height={160}
yLabel="MB"
referenceLine={maxHeapMb} // Add reference line at max heap
referenceLabel="Max Heap"
/>
```
If the chart component doesn't support reference lines, this may need to be deferred or the component extended.
- [ ]**Step 4: Fix agent state "UNKNOWN" display**
Find where the dual state (LIVE + UNKNOWN) is displayed. In the agent detail header area, there's likely a state badge showing both the agent state and a container state.
If the secondary state is "UNKNOWN" while the primary is "LIVE", hide it:
The `\u00b7` and `\u2014` in template literals should render correctly as actual characters. But if they're showing literally, it means they're likely being escaped somewhere upstream. Check if `role.description` contains literal `\u00b7` strings from the backend.
If the backend returns literal `\\u00b7` (double-escaped), the fix is on the backend in the role seed data or the API serialization. If it's a frontend template issue, the existing code should work (JS template literals process unicode escapes at parse time).
Replace ad-hoc empty state patterns with the DS `EmptyState` component or a consistent pattern. Import from the design system:
```typescript
import { EmptyState } from '@cameleer/design-system';
```
In `AppsTab.tsx`, replace all `<p className={styles.emptyNote}>...</p>` with:
```tsx
<EmptyState title="No deployments" description="Deploy this application to see deployment history." />
```
In `GroupsTab.tsx`, replace `<span className={styles.inheritedNote}>(no members)</span>` with:
```tsx
<EmptyState title="No members" description="Add members to this group." />
```
In `RouteDetail.tsx`, replace `<div className={styles.emptyText}>...</div>` and `<div className={styles.emptyState}>...</div>` with `EmptyState` component.
Use the same pattern everywhere: `<EmptyState title="No X" description="..." />`
- [ ]**Step 3: Verify**
1. OIDC page: client secret is masked by default, eye icon toggles visibility
2. Empty states across all pages show consistent centered component
if (Math.abs(value) >= 1_000_000) return `${(value / 1_000_000).toFixed(decimals)}M ${unit}`;
if (Math.abs(value) >= 1_000) return `${(value / 1_000).toFixed(decimals)}K ${unit}`;
if (Number.isInteger(value)) return `${value} ${unit}`;
return `${value.toFixed(decimals)} ${unit}`;
}
export function formatCount(value: number): string {
if (value >= 1_000_000) return `${(value / 1_000_000).toFixed(1)}M`;
if (value >= 1_000) return `${(value / 1_000).toFixed(1)}K`;
return String(value);
}
export function formatPercent(value: number, decimals = 1): string {
return `${value.toFixed(decimals)} %`;
}
```
- [ ]**Step 2: Apply to key display locations**
Search for locations where numbers are displayed with units and use the shared formatters. Focus on the KPI strips and dashboard metrics where inconsistencies were observed.
```bash
grep -rn "msg/s\|/s\|ms\b" ui/src/pages/ --include="*.tsx" | head -20
```
Update the most visible locations to use consistent formatting with space before unit.
**Note:** These items are in the `cameleer-saas` repository, not `cameleer-server`. If the SaaS platform UI code is in a separate repo, this task needs to be executed there. If it's co-located, proceed with these files.
In `AppsTab.tsx`, add a Status column to the app list DataTable showing the deployment status (RUNNING/STOPPED/FAILED) as a colored badge. This data may need to be fetched with the app list or joined from deployments.
- [ ]**Step 2: Breadcrumb update on exchange selection**
In `ExchangesPage.tsx`, when an exchange is selected, update the breadcrumb to show: All Applications > {appName} > Exchange ...{last8chars}
- [ ]**Step 3: Close button on exchange detail panel**
Add an X button or "Close" button to the top-right of the exchange detail panel for explicit dismissal.
- [ ]**Step 4: Command palette exchange ID truncation**
Find the command palette component and apply the same `...{last8chars}` truncation pattern used in the exchange table.
In the DashboardTab heatmap component, check if data spans less than 2 days. If so, show an overlay or message: "More data needed — heatmap requires at least 2 days of history."
- [ ]**Step 6: Commit all nice-to-have changes**
```bash
git add -A ui/src/
git commit -m "fix: nice-to-have polish — status badges, breadcrumbs, close button, heatmap message"