diff --git a/docs/superpowers/plans/2026-04-09-ux-polish-plan.md b/docs/superpowers/plans/2026-04-09-ux-polish-plan.md new file mode 100644 index 00000000..8dbefa39 --- /dev/null +++ b/docs/superpowers/plans/2026-04-09-ux-polish-plan.md @@ -0,0 +1,1688 @@ +# UX Polish & Bug Fixes — Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** 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. + +**Tech Stack:** React 18, TypeScript, CSS Modules, `@cameleer/design-system`, React Router v6, React Query, Spring Boot (backend) + +**Spec:** `docs/superpowers/specs/2026-04-09-ux-polish-design.md` + +**Audit artifacts:** `audit/design-consistency-findings.md`, `audit/interaction-patterns-findings.md`, `audit/monitoring-pages-findings.md`, `audit/admin-lifecycle-findings.md` + +--- + +## Task 1: Fix `/server/deployments` 404 and GC Pauses chart + +**Spec items:** 1.3, 1.4 + +**Files:** +- Modify: `ui/src/router.tsx` +- Modify: `ui/src/pages/AgentInstance/AgentInstance.tsx` + +- [ ] **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: + +```tsx +{ path: 'deployments', element: }, +``` + +Add this alongside the existing legacy redirects. The `Navigate` component should already be imported from `react-router-dom`. + +- [ ] **Step 2: Fix GC Pauses chart X-axis in AgentInstance.tsx** + +In `ui/src/pages/AgentInstance/AgentInstance.tsx`, find the GC Pauses series builder (around line 113): + +```typescript +// BEFORE (line 113): +return [{ label: 'GC ms', data: pts.map((p: any) => ({ x: String(p.time ?? ''), y: p.value })) }]; +``` + +Change to use numeric index like all other charts: + +```typescript +// AFTER: +return [{ label: 'GC ms', data: pts.map((p: any, i: number) => ({ x: i, y: p.value })) }]; +``` + +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:///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 4: Commit** + +```bash +git add ui/src/router.tsx ui/src/pages/AgentInstance/AgentInstance.tsx +git commit -m "fix: add /deployments redirect and fix GC Pauses chart X-axis" +``` + +--- + +## Task 2: Fix user creation in OIDC mode + +**Spec items:** 1.2 + +**Files:** +- Modify: `cameleer3-server-app/src/main/java/com/cameleer3/server/app/controller/UserAdminController.java` +- Modify: `ui/src/pages/Admin/UsersTab.tsx` + +- [ ] **Step 1: Run impact analysis on UserAdminController.createUser** + +```bash +npx gitnexus impact --target "UserAdminController.createUser" --direction upstream +``` + +Review blast radius before editing. + +- [ ] **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 && ( + + Local user creation is disabled while OIDC is enabled. + Switch to OIDC to pre-register a user, or disable OIDC first. + +)} +``` + +Also update the error toast handler (in `handleCreate`) to surface the API error message. Find the catch block and use the response body: + +```typescript +onError: (err: any) => { + const message = err?.body?.error || err?.message || 'Unknown error'; + toast({ title: 'Failed to create user', description: message, variant: 'error', duration: 86_400_000 }); +}, +``` + +- [ ] **Step 4: Verify visually** + +1. Navigate to Admin > Users & Roles +2. Click "+ Add user", select "Local" provider +3. If OIDC is enabled, the info callout should appear +4. Attempting to create should show the descriptive error message, not just "Failed to create user" + +- [ ] **Step 5: Commit** + +```bash +git add cameleer3-server-app/src/main/java/com/cameleer3/server/app/controller/UserAdminController.java ui/src/pages/Admin/UsersTab.tsx +git commit -m "fix: show descriptive error when creating local user with OIDC enabled" +``` + +--- + +## Task 3: Investigate and fix SSE navigation bug + +**Spec item:** 1.1 + +**Files:** +- Investigate: `ui/src/components/LayoutShell.tsx` +- Investigate: `ui/src/pages/Exchanges/ExchangesPage.tsx` +- 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 + +**Spec items:** 2a.1, 2a.4 + +**Files:** +- Modify: `ui/src/pages/Dashboard/Dashboard.tsx` +- Modify: `ui/src/pages/Dashboard/Dashboard.module.css` +- Modify: `ui/src/pages/DashboardTab/DashboardTab.module.css` +- Modify: `ui/src/pages/AppsTab/AppsTab.module.css` + +- [ ] **Step 1: Wrap exchanges table in shared table-section** + +In `ui/src/pages/Dashboard/Dashboard.tsx`, add the shared table-section import: + +```typescript +import tableStyles from '../../styles/table-section.module.css'; +``` + +Find the table rendering section (around line 237-285). Wrap the table header and DataTable in the shared `tableSection`: + +```tsx +
+
+ Recent Exchanges +
+ {exchanges.length} of {formatNumber(total)} exchanges + {/* existing auto-refresh indicator */} +
+
+ +
+``` + +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`: + +```css +/* BEFORE: */ +.content { + display: flex; + flex-direction: column; + gap: 20px; + flex: 1; + min-height: 0; + overflow-y: auto; + padding-bottom: 20px; +} + +/* AFTER: */ +.content { + display: flex; + flex-direction: column; + gap: 20px; + flex: 1; + min-height: 0; + overflow-y: auto; + padding: 0 24px 20px; +} +``` + +- [ ] **Step 4: Normalize AppsTab container padding** + +In `ui/src/pages/AppsTab/AppsTab.module.css`, update `.container`: + +```css +/* BEFORE: */ +.container { + padding: 16px; + overflow-y: auto; + flex: 1; +} + +/* AFTER: */ +.container { + padding: 20px 24px 40px; + overflow-y: auto; + flex: 1; +} +``` + +- [ ] **Step 5: Verify visually** + +1. Exchanges tab: table should have card wrapper with border/shadow, matching Audit Log style +2. Dashboard: content should have side margins (24px), no longer flush against sidebar +3. Deployments tab: spacing should match Admin pages (20px top, 24px sides) + +- [ ] **Step 6: Commit** + +```bash +git add ui/src/pages/Dashboard/Dashboard.tsx ui/src/pages/Dashboard/Dashboard.module.css ui/src/pages/DashboardTab/DashboardTab.module.css ui/src/pages/AppsTab/AppsTab.module.css +git commit -m "fix: standardize table containment and container padding across pages" +``` + +--- + +## Task 5: App detail section cards and deployment DataTable + +**Spec items:** 2a.2, 2a.3 + +**Files:** +- Modify: `ui/src/pages/AppsTab/AppsTab.tsx` +- Modify: `ui/src/pages/AppsTab/AppsTab.module.css` + +- [ ] **Step 1: Import shared section-card and table-section modules** + +At the top of `ui/src/pages/AppsTab/AppsTab.tsx`, add: + +```typescript +import sectionStyles from '../../styles/section-card.module.css'; +import tableStyles from '../../styles/table-section.module.css'; +``` + +- [ ] **Step 2: Wrap config sub-tab content in section cards** + +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 +
+ Monitoring + {/* existing monitoring controls: Engine Level, Payload Capture, etc. */} +
+``` + +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 `` with DataTable in OverviewSubTab** + +Find the manual `
` in `OverviewSubTab` (lines 623-680). Replace with: + +```tsx +
+
+ Deployments +
+ {row.environmentSlug} }, + { key: 'version', header: 'Version' }, + { key: 'status', header: 'Status', render: (_, row) => {row.status} }, + { key: 'deployStage', header: 'Deploy Stage' }, + { key: 'actions', header: '', render: (_, row) => ( + row.status === 'RUNNING' || row.status === 'STARTING' ? ( + + ) : null + )}, + ]} + data={deployments} + emptyMessage="No deployments yet." + /> +
+``` + +Adapt the column definitions to match the existing manual table columns. Import `DataTable` from `@cameleer/design-system` if not already imported. + +- [ ] **Step 4: Remove custom `.table` styles from AppsTab.module.css** + +Remove the manual table CSS classes (`.table`, `.table th`, `.table td`, etc.) from `AppsTab.module.css` since they're replaced by DataTable + shared table-section. + +- [ ] **Step 5: Verify visually** + +1. Navigate to Deployments tab > click an app +2. Configuration sections should have card wrappers (border, shadow, background) +3. Deployment table should use DataTable with card wrapper, matching other tables + +- [ ] **Step 6: Commit** + +```bash +git add ui/src/pages/AppsTab/AppsTab.tsx ui/src/pages/AppsTab/AppsTab.module.css +git commit -m "fix: wrap app config in section cards, replace manual table with DataTable" +``` + +--- + +## Task 6: Deduplicate card CSS and wrap remaining flat content + +**Spec items:** 2a.5, 2a.6, 2a.7 + +**Files:** +- Modify: `ui/src/pages/DashboardTab/DashboardTab.module.css` +- Modify: `ui/src/pages/DashboardTab/DashboardL1.tsx` (or L2/L3 — whichever uses errorsSection/diagramSection) +- Modify: `ui/src/pages/AgentHealth/AgentHealth.tsx` +- Modify: `ui/src/pages/AgentHealth/AgentHealth.module.css` +- Modify: `ui/src/pages/AgentInstance/AgentInstance.tsx` +- Modify: `ui/src/pages/AgentInstance/AgentInstance.module.css` +- Modify: `ui/src/pages/Admin/ClickHouseAdminPage.module.css` +- Modify: `ui/src/pages/Admin/ClickHouseAdminPage.tsx` +- Modify: `ui/src/pages/Admin/DatabaseAdminPage.tsx` +- Modify: `ui/src/pages/Admin/UsersTab.tsx` +- Modify: `ui/src/pages/Admin/GroupsTab.tsx` +- Modify: `ui/src/pages/Admin/EnvironmentsPage.tsx` + +- [ ] **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: + +```tsx +
+``` + +```css +.diagramHeight { height: 280px; } +``` + +- [ ] **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: +
+ Group Membership + {/* existing membership tags */} +
+ +
+ Effective Roles + {/* existing role tags */} +
+``` + +- [ ] **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 + +- [ ] **Step 8: Commit** + +```bash +git add -A ui/src/pages/DashboardTab/ ui/src/pages/AgentHealth/ ui/src/pages/AgentInstance/ ui/src/pages/Admin/ClickHouseAdminPage.* ui/src/pages/Admin/DatabaseAdminPage.* ui/src/pages/Admin/UsersTab.tsx ui/src/pages/Admin/GroupsTab.tsx ui/src/pages/Admin/EnvironmentsPage.tsx +git commit -m "fix: deduplicate card CSS, use shared section-card and table-section modules" +``` + +--- + +## Task 7: Button order, confirmation dialogs, and destructive action guards + +**Spec items:** 2b.1, 2b.2, 2b.3, 2b.4, 2b.5, 2b.6, 2b.8 + +**Files:** +- Modify: `ui/src/pages/Admin/AppConfigDetailPage.tsx` +- Modify: `ui/src/pages/AppsTab/AppsTab.tsx` +- Modify: `ui/src/components/TapConfigModal.tsx` +- Modify: `ui/src/pages/Admin/DatabaseAdminPage.tsx` +- Modify: `ui/src/pages/Admin/UsersTab.tsx` +- Modify: `ui/src/pages/Routes/RouteDetail.tsx` +- Modify: `ui/src/pages/Admin/OidcConfigPage.tsx` + +- [ ] **Step 1: Fix AppConfigDetailPage button order** + +In `ui/src/pages/Admin/AppConfigDetailPage.tsx`, find lines 310-319. Change from Save|Cancel to Cancel|Save with correct variants: + +```tsx +{editing ? ( +
+ + +
+) : ( + +)} +``` + +This also fixes spec items 2b.13 (uses `loading` prop instead of "Saving..." text). + +- [ ] **Step 2: Add confirmation dialog for deployment stop** + +In `ui/src/pages/AppsTab/AppsTab.tsx`, find `handleStop` (around line 526). Add state for the confirmation dialog: + +```typescript +const [stopTarget, setStopTarget] = useState<{ id: string; name: string } | null>(null); +``` + +Replace the immediate stop with a dialog trigger: + +```tsx +// In the OverviewSubTab, change the Stop button to: + + +// Add ConfirmDialog near the bottom of the component: + setStopTarget(null)} + onConfirm={async () => { + if (!stopTarget) return; + try { + await stopDeployment.mutateAsync({ appId: appSlug, deploymentId: stopTarget.id }); + toast({ title: 'Deployment stopped', variant: 'warning' }); + } catch { + toast({ title: 'Failed to stop deployment', variant: 'error', duration: 86_400_000 }); + } + setStopTarget(null); + }} + title="Stop deployment?" + message={`Stop deployment for "${stopTarget?.name}"? This will take the service offline.`} + confirmText={appSlug} + loading={stopDeployment.isPending} +/> +``` + +Import `ConfirmDialog` from `@cameleer/design-system` if not already imported. + +- [ ] **Step 3: Add confirmation dialog for tap deletion in TapConfigModal** + +In `ui/src/components/TapConfigModal.tsx`, find the delete handler (around line 117). Add state: + +```typescript +const [showDeleteConfirm, setShowDeleteConfirm] = useState(false); +``` + +Change the delete button (around line 249) from immediate to dialog trigger: + +```tsx + + + setShowDeleteConfirm(false)} + onConfirm={() => { + onDelete?.(); + setShowDeleteConfirm(false); + onClose(); + }} + title="Delete tap?" + message={`Delete tap "${attributeName}"? This will remove it from the configuration.`} + confirmText={attributeName} +/> +``` + +- [ ] **Step 4: Add AlertDialog for kill query in DatabaseAdminPage** + +In `ui/src/pages/Admin/DatabaseAdminPage.tsx`, find the Kill button (around line 30). Add state and dialog: + +```typescript +const [killTarget, setKillTarget] = useState(null); +``` + +```tsx + + + setKillTarget(null)} + onConfirm={async () => { + try { + await killQuery(killTarget!); + toast({ title: 'Query killed', variant: 'warning' }); + } catch { + toast({ title: 'Failed to kill query', variant: 'error', duration: 86_400_000 }); + } + setKillTarget(null); + }} + title="Kill query?" + description={`This will terminate the running query (PID: ${killTarget}). Continue?`} + confirmLabel="Kill" + variant="warning" +/> +``` + +Import `AlertDialog` from `@cameleer/design-system`. + +- [ ] **Step 5: Add AlertDialog for role removal from user** + +In `ui/src/pages/Admin/UsersTab.tsx`, find the role tag `onRemove` handler (around line 508-526). Add state: + +```typescript +const [removeRoleTarget, setRemoveRoleTarget] = useState<{ userId: string; roleId: string; roleName: string } | null>(null); +``` + +Change the `onRemove` to open a dialog instead of immediate mutation: + +```tsx +onRemove={() => setRemoveRoleTarget({ userId: selected.userId, roleId: r.id, roleName: r.name })} +``` + +Add the AlertDialog: + +```tsx + setRemoveRoleTarget(null)} + onConfirm={() => { + if (!removeRoleTarget) return; + removeRole.mutate( + { userId: removeRoleTarget.userId, roleId: removeRoleTarget.roleId }, + { + onSuccess: () => toast({ title: 'Role removed', description: removeRoleTarget.roleName, variant: 'success' }), + onError: () => toast({ title: 'Failed to remove role', variant: 'error', duration: 86_400_000 }), + }, + ); + setRemoveRoleTarget(null); + }} + title="Remove role?" + description={`Remove role "${removeRoleTarget?.roleName}"? This may revoke access for this user. Continue?`} + confirmLabel="Remove" + variant="warning" +/> +``` + +- [ ] **Step 6: Standardize Cancel button variant to ghost** + +In `ui/src/components/TapConfigModal.tsx` (around line 255), change: +```tsx +// BEFORE: + +// AFTER: + +``` + +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 + +``` + +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) +2. Deployments: Stop button shows type-to-confirm dialog +3. TapConfigModal: Delete shows confirmation dialog +4. Database: Kill shows AlertDialog with warning +5. Users: Removing a role shows AlertDialog +6. All Cancel buttons use ghost variant + +- [ ] **Step 9: Commit** + +```bash +git add ui/src/pages/Admin/AppConfigDetailPage.tsx ui/src/pages/AppsTab/AppsTab.tsx ui/src/components/TapConfigModal.tsx ui/src/pages/Admin/DatabaseAdminPage.tsx ui/src/pages/Admin/UsersTab.tsx ui/src/pages/Routes/RouteDetail.tsx ui/src/pages/Admin/OidcConfigPage.tsx +git commit -m "fix: standardize button order, add confirmation dialogs for destructive actions" +``` + +--- + +## Task 8: OIDC edit mode, loading states, and error toast format + +**Spec items:** 2b.7, 2b.9, 2b.10, 2b.13, 2b.14 + +**Files:** +- Modify: `ui/src/pages/Admin/OidcConfigPage.tsx` +- Modify: `ui/src/pages/Admin/UsersTab.tsx` +- Modify: `ui/src/pages/Admin/GroupsTab.tsx` +- Modify: `ui/src/pages/Admin/RolesTab.tsx` +- Modify: `ui/src/pages/Admin/EnvironmentsPage.tsx` +- Modify: `ui/src/pages/AppsTab/AppsTab.tsx` +- Modify: `ui/src/pages/Admin/AppConfigDetailPage.tsx` + +- [ ] **Step 1: Add edit mode to OidcConfigPage** + +In `ui/src/pages/Admin/OidcConfigPage.tsx`, add editing state: + +```typescript +const [editing, setEditing] = useState(false); +const [formDraft, setFormDraft] = useState(null); + +function startEditing() { + setFormDraft(form ? { ...form } : null); + setEditing(true); +} + +function cancelEditing() { + setFormDraft(null); + setEditing(false); + setError(null); +} +``` + +Update the toolbar (around line 130-137): +```tsx +{editing ? ( + <> + + + +) : ( + <> + + + +)} +``` + +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 `` (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 `` returns with ``: + +**UsersTab.tsx** — find `if (usersLoading) return ;` and replace: +```typescript +import { PageLoader } from '../../components/PageLoader'; +// ... +if (usersLoading) return ; +``` + +**GroupsTab.tsx** — same pattern: +```typescript +import { PageLoader } from '../../components/PageLoader'; +if (groupsLoading) return ; +``` + +**RolesTab.tsx:** +```typescript +import { PageLoader } from '../../components/PageLoader'; +if (rolesLoading) return ; +``` + +**EnvironmentsPage.tsx:** +```typescript +import { PageLoader } from '../../components/PageLoader'; +if (envsLoading) return ; +``` + +**OidcConfigPage.tsx** — currently returns `null`, change to: +```typescript +import { PageLoader } from '../../components/PageLoader'; +if (!form) return ; +``` + +**AppsTab.tsx** (AppListView and AppDetailView) — find `` returns and replace with ``. + +- [ ] **Step 3: Standardize error toast titles** + +In `ui/src/pages/AppsTab/AppsTab.tsx`, find error toasts with format "[Noun] failed" and change to "Failed to [verb] [noun]": + +- `'Save failed'` -> `'Failed to save configuration'` +- `'Upload failed'` -> `'Failed to upload JAR'` +- `'Deploy failed'` -> `'Failed to deploy application'` +- `'Stop failed'` -> `'Failed to stop deployment'` + +In `ui/src/pages/Admin/AppConfigDetailPage.tsx`: +- `'Save failed'` -> `'Failed to save configuration'` + +- [ ] **Step 4: Verify** + +1. OIDC page: starts in read-only mode, Edit button enters edit mode, Cancel discards changes +2. All admin pages show centered PageLoader spinner (not small inline Spinner) +3. Error toasts use "Failed to [verb] [noun]" format + +- [ ] **Step 5: Commit** + +```bash +git add ui/src/pages/Admin/OidcConfigPage.tsx ui/src/pages/Admin/UsersTab.tsx ui/src/pages/Admin/GroupsTab.tsx ui/src/pages/Admin/RolesTab.tsx ui/src/pages/Admin/EnvironmentsPage.tsx ui/src/pages/AppsTab/AppsTab.tsx ui/src/pages/Admin/AppConfigDetailPage.tsx +git commit -m "fix: add OIDC edit mode, standardize PageLoader and error toast format" +``` + +--- + +## Task 9: WCAG contrast fixes and font size floor + +**Spec items:** 3.1, 3.2, 3.3 + +**Files:** +- Modify: Design system theme CSS (find the file defining `--text-muted` and `--text-faint`) +- Modify: Multiple CSS modules (font-size changes) + +- [ ] **Step 1: Find where design system tokens are defined** + +```bash +cd ui && grep -rn "\-\-text-muted" node_modules/@cameleer/design-system/dist/ 2>/dev/null | head -5 +# Or check if tokens are defined in the project itself: +grep -rn "\-\-text-muted" src/ --include="*.css" | grep -v "module.css" | head -10 +``` + +If the tokens are in the design system package, they need to be overridden at the app level. If they're in a local theme file, edit directly. + +- [ ] **Step 2: Update --text-muted values** + +In the theme/token CSS file (wherever `--text-muted` is defined): + +```css +/* Light mode: */ +--text-muted: #766A5E; /* was #9C9184, now 4.5:1 on white */ + +/* Dark mode: */ +--text-muted: #9A9088; /* was #7A7068, now 4.5:1 on #242019 */ +``` + +- [ ] **Step 3: Update --text-faint dark mode value** + +```css +/* Dark mode: */ +--text-faint: #6A6058; /* was #4A4238 (1.4:1!), now 3:1 on #242019 */ +``` + +- [ ] **Step 4: Audit and fix --text-faint usage on readable text** + +```bash +grep -rn "text-faint" ui/src/ --include="*.css" --include="*.module.css" +``` + +For each usage, check if it's on readable text (not just decorative borders/dividers). If on readable text, change to `--text-muted`. + +- [ ] **Step 5: Fix all sub-12px font sizes** + +```bash +grep -rn "font-size: 10px\|font-size: 11px" ui/src/ --include="*.css" --include="*.module.css" +``` + +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 { + if (ms >= 60_000) { + const minutes = Math.floor(ms / 60_000); + const seconds = Math.round((ms % 60_000) / 1000); + return seconds > 0 ? `${minutes}m ${seconds}s` : `${minutes}m`; + } + if (ms >= 1000) return `${(ms / 1000).toFixed(1)}s`; + return `${ms}ms`; +} +``` + +Changes: `>= 60s` now shows `Xm Ys` instead of raw seconds. `1-60s` now shows one decimal (`6.7s`) instead of two (`6.70s`). + +Also update `formatDurationShort` to match: + +```typescript +export function formatDurationShort(ms: number | undefined): string { + if (ms == null) return '-'; + if (ms >= 60_000) { + const minutes = Math.floor(ms / 60_000); + const seconds = Math.round((ms % 60_000) / 1000); + return seconds > 0 ? `${minutes}m ${seconds}s` : `${minutes}m`; + } + if (ms >= 1000) return `${(ms / 1000).toFixed(1)}s`; + return `${ms}ms`; +} +``` + +- [ ] **Step 2: Truncate Exchange IDs in the table** + +In `ui/src/pages/Dashboard/Dashboard.tsx`, find the Exchange ID column (around line 114-116): + +```typescript +// BEFORE: +render: (_: unknown, row: Row) => ( + {row.executionId} +), +``` + +Change to truncated display with tooltip: + +```typescript +// AFTER: +render: (_: unknown, row: Row) => ( + { + e.stopPropagation(); + navigator.clipboard.writeText(row.executionId); + }}> + ...{row.executionId.slice(-8)} + +), +``` + +This shows the last 8 characters with an ellipsis prefix, the full ID on hover, and copies to clipboard on click. + +- [ ] **Step 3: Verify** + +1. Exchange table: IDs should show `...0001E75C` format +2. Hovering should show full ID +3. Clicking the ID should copy to clipboard +4. Durations: `321000ms` should show as `5m 21s`, `6700ms` as `6.7s`, `178ms` as `178ms` + +- [ ] **Step 4: Commit** + +```bash +git add ui/src/utils/format-utils.ts ui/src/pages/Dashboard/Dashboard.tsx +git commit -m "fix: improve duration formatting (Xm Ys) and truncate exchange IDs" +``` + +--- + +## 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: + +```typescript +const hasAttributes = exchanges.some(e => e.attributes && Object.keys(e.attributes).length > 0); +``` + +In the columns array, conditionally include the Attributes column: + +```typescript +const columns = [ + // ...status, route, application columns... + ...(hasAttributes ? [{ + key: 'attributes' as const, + header: 'Attributes', + render: (_: unknown, row: Row) => { + const attrs = row.attributes; + if (!attrs || Object.keys(attrs).length === 0) return ; + return ( + + {Object.entries(attrs).map(([k, v]) => ( + {k}={v} + ))} + + ); + }, + }] : []), + // ...Exchange ID, Started, Duration, Agent columns... +]; +``` + +- [ ] **Step 2: Standardize status labels** + +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: +```bash +grep -rn "COMPLETED\|FAILED" ui/src/pages/Exchanges/ --include="*.tsx" +``` + +Update any raw status display to use `statusLabel()`. + +- [ ] **Step 3: Truncate agent names** + +In `ui/src/pages/Dashboard/Dashboard.tsx`, find the Agent column (around line 135-140). Add a truncation helper: + +```typescript +function shortAgentName(name: string): string { + // If name contains multiple dashes (K8s pod name pattern), take the last segment + const parts = name.split('-'); + if (parts.length >= 3) { + // Show last 2 segments: "8c0affadb860-1" from "cameleer3-sample-8c0affadb860-1" + return parts.slice(-2).join('-'); + } + return name; +} +``` + +Update the Agent column render: + +```typescript +render: (_: unknown, row: Row) => ( + {shortAgentName(row.agentId)} +), +``` + +- [ ] **Step 4: Verify** + +1. Exchange table: Attributes column should be hidden (all "---" currently) +2. Status shows "OK"/"ERR" in both table and detail panel +3. Agent names show truncated form with full name on hover + +- [ ] **Step 5: Commit** + +```bash +git add ui/src/pages/Dashboard/Dashboard.tsx ui/src/utils/format-utils.ts ui/src/pages/Exchanges/ +git commit -m "fix: hide empty attributes column, standardize status labels, truncate agent names" +``` + +--- + +## Task 12: Chart Y-axis scaling and agent state display + +**Spec items:** 5.1, 5.2, 5.3, 5.4, 5.5 + +**Files:** +- Modify: `ui/src/pages/AgentInstance/AgentInstance.tsx` +- Modify: `ui/src/pages/DashboardTab/DashboardTab.module.css` + +- [ ] **Step 1: Fix agent chart Y-axis auto-scaling** + +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: +const throughputMax = Math.max(...throughputSeries[0].data.map(d => d.y), 1) * 1.2; +``` + +Pass this to the chart: `yMax={throughputMax}` + +Apply the same pattern to Error Rate (line 353) and all other charts. + +- [ ] **Step 2: Standardize Error Rate unit** + +Find the Error Rate chart (around line 353). Change `yLabel` from `"err/h"` to match the KPI display: + +```typescript +// BEFORE: + + +// AFTER: + +``` + +Ensure the error rate data is in percentage format (not absolute count). If the data is in errors/hour, convert: +```typescript +// Convert errors per hour to percentage: +const errorPctSeries = errorSeries.map(s => ({ + ...s, + data: s.data.map(d => ({ ...d, y: totalThroughput > 0 ? (d.y / totalThroughput * 100) : 0 })), +})); +``` + +- [ ] **Step 3: Add memory reference line** + +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 + +``` + +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: + +```tsx +{agent.state && agent.state !== 'UNKNOWN' && agent.state !== agent.primaryState && ( + {agent.state} +)} +``` + +Or add a label: `Container: {agent.containerState}` + +- [ ] **Step 5: Fix Dashboard table pointer events** + +In `ui/src/pages/DashboardTab/DashboardTab.module.css`, find `.chartGrid` or `._tableSection` classes. Add explicit pointer-events and z-index: + +```css +/* Ensure table rows are clickable above chart overlays */ +.tableSection { + position: relative; + z-index: 1; +} + +.chartGrid { + pointer-events: none; /* Don't intercept clicks meant for the table */ +} + +.chartGrid > * { + pointer-events: auto; /* But chart elements themselves are still interactive */ +} +``` + +The exact fix depends on the DOM structure. Inspect the layout to see which element is intercepting clicks. + +- [ ] **Step 6: Verify** + +1. Agent Throughput chart: Y-axis scales to actual data range (not 1.2k when data is ~2) +2. Agent Error Rate chart: shows "%" label +3. Agent Memory chart: shows reference line at max heap +4. Agent state: no confusing "UNKNOWN" alongside "LIVE" +5. Dashboard: Application Health table rows clickable without CSS interception + +- [ ] **Step 7: Commit** + +```bash +git add ui/src/pages/AgentInstance/AgentInstance.tsx ui/src/pages/DashboardTab/DashboardTab.module.css +git commit -m "fix: chart Y-axis auto-scaling, error rate unit, memory reference line, pointer events" +``` + +--- + +## Task 13: Error toast API details, unicode fix, password confirmation + +**Spec items:** 6.1, 6.2, 6.3 + +**Files:** +- Modify: `ui/src/pages/Admin/UsersTab.tsx` +- Modify: `ui/src/pages/Admin/RolesTab.tsx` +- Modify: Multiple files with API error handlers + +- [ ] **Step 1: Surface API error details in toasts** + +Create a shared error extraction utility. In `ui/src/utils/format-utils.ts`, add: + +```typescript +export async function extractApiError(err: unknown): Promise { + if (err && typeof err === 'object') { + const e = err as any; + // If the error has a response body with a message + if (e.body?.error) return e.body.error; + if (e.body?.message) return e.body.message; + if (e.message) return e.message; + // Try to read the response body if it's a fetch Response + if (e.response?.text) { + try { + const text = await e.response.text(); + const json = JSON.parse(text); + return json.error || json.message || text; + } catch { return 'Unknown error'; } + } + } + return 'Unknown error'; +} +``` + +Update error toast handlers in key files to use the description field. For example in `UsersTab.tsx`: + +```typescript +onError: async (err) => { + const description = await extractApiError(err); + toast({ title: 'Failed to create user', description, variant: 'error', duration: 86_400_000 }); +}, +``` + +Apply this pattern to the most visible error handlers across RBAC pages, AppsTab, and AppConfigDetailPage. + +- [ ] **Step 2: Fix unicode escape in role descriptions** + +In `ui/src/pages/Admin/RolesTab.tsx`, find line 180: + +```typescript +// BEFORE: +{role.description || '\u2014'} \u00b7 {getAssignmentCount(role)} assignments +``` + +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). + +Check what the API returns: +```bash +curl -s -H "Authorization: Bearer " https:///api/v1/admin/roles | jq '.[].description' +``` + +If the backend returns literal `\u00b7`, fix the seed data or migration that creates the system roles. + +- [ ] **Step 3: Add password confirmation field** + +In `ui/src/pages/Admin/UsersTab.tsx`, find the create form (around line 238-252). Add a confirm password field: + +```typescript +const [newPasswordConfirm, setNewPasswordConfirm] = useState(''); +const passwordMismatch = newPassword.length > 0 && newPasswordConfirm.length > 0 && newPassword !== newPasswordConfirm; +``` + +After the existing password input: + +```tsx +{newProvider === 'local' && ( + <> + setNewPassword(e.target.value)} + /> + setNewPasswordConfirm(e.target.value)} + /> + {passwordMismatch && Passwords do not match} + Min 12 characters, 3 of 4: uppercase, lowercase, number, special + +)} +``` + +Add the `passwordMismatch` check to the Create button's `disabled` condition: + +```tsx +disabled={!newUsername.trim() || (newProvider === 'local' && (!newPassword.trim() || passwordMismatch)) || duplicateUsername} +``` + +Reset `newPasswordConfirm` when the form closes. + +Add `.hintText` to the CSS module: +```css +.hintText { + font-size: 12px; + color: var(--text-muted); +} +``` + +- [ ] **Step 4: Verify** + +1. Create user form: shows password confirmation field, validation message on mismatch, policy hint +2. API errors show descriptive messages in toasts +3. Role descriptions show rendered characters (middle dot), not escape sequences + +- [ ] **Step 5: Commit** + +```bash +git add ui/src/utils/format-utils.ts ui/src/pages/Admin/UsersTab.tsx ui/src/pages/Admin/RolesTab.tsx ui/src/pages/AppsTab/AppsTab.tsx ui/src/pages/Admin/AppConfigDetailPage.tsx +git commit -m "fix: surface API errors in toasts, fix unicode in roles, add password confirmation" +``` + +--- + +## Task 14: OIDC client secret masking and empty state standardization + +**Spec items:** 6.6, 2b.11 + +**Files:** +- Modify: `ui/src/pages/Admin/OidcConfigPage.tsx` +- Modify: `ui/src/pages/AppsTab/AppsTab.tsx` +- Modify: `ui/src/pages/Admin/UsersTab.tsx` +- Modify: `ui/src/pages/Admin/GroupsTab.tsx` +- Modify: `ui/src/pages/Routes/RouteDetail.tsx` + +- [ ] **Step 1: Mask OIDC client secret** + +In `ui/src/pages/Admin/OidcConfigPage.tsx`, find the Client Secret input field. Add a show/hide toggle: + +```typescript +const [showSecret, setShowSecret] = useState(false); +``` + +```tsx +
+ updateField('clientSecret', e.target.value)} + readOnly={!editing} + /> + +
+``` + +Import `Eye` and `EyeOff` from `lucide-react`. + +- [ ] **Step 2: Standardize empty states** + +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 `

...

` with: +```tsx + +``` + +In `GroupsTab.tsx`, replace `(no members)` with: +```tsx + +``` + +In `RouteDetail.tsx`, replace `
...
` and `
...
` with `EmptyState` component. + +Use the same pattern everywhere: `` + +- [ ] **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 + +- [ ] **Step 4: Commit** + +```bash +git add ui/src/pages/Admin/OidcConfigPage.tsx ui/src/pages/AppsTab/AppsTab.tsx ui/src/pages/Admin/UsersTab.tsx ui/src/pages/Admin/GroupsTab.tsx ui/src/pages/Routes/RouteDetail.tsx +git commit -m "fix: mask OIDC client secret, standardize empty states across pages" +``` + +--- + +## Task 15: Number formatting and locale consistency + +**Spec items:** 4.6 + +**Files:** +- Modify: `ui/src/utils/format-utils.ts` +- Modify: Files that display numbers with units + +- [ ] **Step 1: Add shared number formatting utility** + +In `ui/src/utils/format-utils.ts`, add: + +```typescript +export function formatMetric(value: number, unit: string, decimals = 1): string { + 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. + +- [ ] **Step 3: Verify** + +1. KPI values show consistent formatting: "6.7 s", "1.9 %", "7.1 msg/s" +2. Large numbers use K/M suffixes consistently + +- [ ] **Step 4: Commit** + +```bash +git add ui/src/utils/format-utils.ts ui/src/pages/ +git commit -m "fix: standardize number formatting with consistent unit spacing and K/M suffixes" +``` + +--- + +## Task 16: Platform label/value spacing and license badge colors (SaaS repo) + +**Spec items:** 6.4, 6.5 + +**Note:** These items are in the `cameleer-saas` repository, not `cameleer3-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. + +- [ ] **Step 1: Identify platform component files** + +```bash +# Check if SaaS platform UI is in this repo: +find . -name "*.tsx" | xargs grep -l "Slugdefault\|Max Agents" 2>/dev/null +# Or search for the platform dashboard component: +grep -rn "Tenant Information\|Server Management" ui/src/ --include="*.tsx" +``` + +If not found in this repo, note that these fixes belong to the `cameleer-saas` repo and skip to commit. + +- [ ] **Step 2: Fix label/value spacing** + +Find the key-value display components. Change from: +```tsx +Slug{tenant.slug} +``` +To: +```tsx +Slug: {tenant.slug} +``` + +Or better, use a definition list pattern: +```tsx +
+
Slug
{tenant.slug}
+
Status
{tenant.status}
+
Created
{formatDate(tenant.createdAt)}
+
+``` + +- [ ] **Step 3: Fix license badge colors** + +Find the license features display. Change DISABLED badges from error/red to neutral: + +```tsx +// BEFORE: +{feature.enabled ? 'ENABLED' : 'DISABLED'} + +// AFTER: +{feature.enabled ? 'ENABLED' : 'NOT INCLUDED'} +``` + +- [ ] **Step 4: Commit** + +```bash +# If in this repo: +git add +git commit -m "fix: platform label/value spacing and neutral license badge colors" +# If in cameleer-saas repo, note the fix for that repo +``` + +--- + +## Task 17: Audit log CSV export + +**Spec items:** 6.7 + +**Files:** +- Modify: `ui/src/pages/Admin/AuditLogPage.tsx` +- Modify: `cameleer3-server-app/src/main/java/com/cameleer3/server/app/controller/AuditLogController.java` + +- [ ] **Step 1: Add client-side CSV export for current page** + +In `ui/src/pages/Admin/AuditLogPage.tsx`, add an export button to the table header area: + +```typescript +function exportCsv(events: AuditEvent[]) { + const headers = ['Timestamp', 'User', 'Category', 'Action', 'Target', 'Result', 'Details']; + const rows = events.map(e => [ + e.timestamp, e.username, e.category, e.action, e.target, e.result, e.details ?? '', + ]); + const csv = [headers, ...rows].map(r => r.map(c => `"${String(c).replace(/"/g, '""')}"`).join(',')).join('\n'); + const blob = new Blob([csv], { type: 'text/csv' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = `cameleer-audit-${new Date().toISOString().slice(0, 16).replace(':', '-')}.csv`; + a.click(); + URL.revokeObjectURL(url); +} +``` + +Add the button in the table header alongside existing controls: + +```tsx + +``` + +Import `Download` from `lucide-react`. + +- [ ] **Step 2: Verify** + +1. Navigate to Admin > Audit Log +2. Click "Export CSV" — should download a CSV file with current page data +3. Open CSV and verify columns match the table + +- [ ] **Step 3: Commit** + +```bash +git add ui/src/pages/Admin/AuditLogPage.tsx +git commit -m "feat: add CSV export to audit log" +``` + +--- + +## Task 18: Unsaved changes indicators (was Task 17) + +**Spec items:** 2b.12 + +**Files:** +- Modify: `ui/src/pages/Admin/AppConfigDetailPage.tsx` +- Modify: `ui/src/pages/Admin/EnvironmentsPage.tsx` + +- [ ] **Step 1: Add unsaved changes banner to AppConfigDetailPage** + +Borrow the banner pattern from AppsTab's ConfigSubTab. In `AppConfigDetailPage.tsx`, add a banner above the form when in edit mode: + +```tsx +{editing && ( +
+ Editing configuration. Changes are not saved until you click Save. +
+)} +``` + +Add the CSS: +```css +.editBanner { + padding: 8px 16px; + background: var(--amber-bg, rgba(198, 130, 14, 0.08)); + border: 1px solid var(--amber); + border-radius: var(--radius-sm); + font-size: 13px; + color: var(--text-primary); + margin-bottom: 16px; +} +``` + +- [ ] **Step 2: Add unsaved changes banner to Environment editing sections** + +In `EnvironmentsPage.tsx`, find the Default Resources and JAR Retention edit sections. When `editing` is true, show the same banner pattern: + +```tsx +{editingResources && ( +
+ Editing resource defaults. Changes are not saved until you click Save. +
+)} +``` + +- [ ] **Step 3: Verify** + +1. AppConfigDetailPage: amber banner appears when in edit mode +2. Environments: amber banner appears when editing resource defaults or JAR retention + +- [ ] **Step 4: Commit** + +```bash +git add ui/src/pages/Admin/AppConfigDetailPage.tsx ui/src/pages/Admin/EnvironmentsPage.tsx +git commit -m "fix: add unsaved changes banners to edit mode forms" +``` + +--- + +## Task 19: Nice-to-have polish (batch 7) + +**Spec items:** 7.1-7.10 (implement time-permitting) + +**Files:** +- Various + +- [ ] **Step 1: Deployment list status badges** + +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. + +- [ ] **Step 5: 7-Day Pattern heatmap insufficient data** + +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" +``` + +--- + +## Summary + +| Task | Batch | Key Changes | Commit | +|------|-------|-------------|--------| +| 1 | 1 (Bugs) | Deployments redirect, GC Pauses chart | `fix: add /deployments redirect and fix GC Pauses chart X-axis` | +| 2 | 1 (Bugs) | User creation OIDC fix (backend + frontend) | `fix: show descriptive error when creating local user with OIDC enabled` | +| 3 | 1 (Bugs) | SSE navigation bug investigation + fix | `fix: prevent SSE data updates from triggering navigation on admin pages` | +| 4 | 2a (Layout) | Exchange table containment, padding normalization | `fix: standardize table containment and container padding across pages` | +| 5 | 2a (Layout) | App detail section cards, deployment DataTable | `fix: wrap app config in section cards, replace manual table with DataTable` | +| 6 | 2a (Layout) | Deduplicate card CSS, wrap flat admin detail sections | `fix: deduplicate card CSS, use shared section-card and table-section modules` | +| 7 | 2b (Interaction) | Button order, confirmation dialogs, destructive guards | `fix: standardize button order, add confirmation dialogs for destructive actions` | +| 8 | 2b (Interaction) | OIDC edit mode, PageLoader, error toast format | `fix: add OIDC edit mode, standardize PageLoader and error toast format` | +| 9 | 3 (Contrast) | WCAG contrast fixes, font size floor | `fix: WCAG AA contrast compliance, 12px font floor` | +| 10 | 4 (Formatting) | Duration formatter, exchange ID truncation | `fix: improve duration formatting, truncate exchange IDs` | +| 11 | 4 (Formatting) | Attributes column, status labels, agent names | `fix: hide empty attributes, standardize status labels, truncate agent names` | +| 12 | 5 (Charts) | Y-axis scaling, error rate unit, memory ref line, pointer events | `fix: chart Y-axis auto-scaling, error rate unit, pointer events` | +| 13 | 6 (Admin) | API error details, unicode fix, password confirmation | `fix: surface API errors, fix unicode in roles, add password confirmation` | +| 14 | 6 (Admin) | OIDC secret masking, empty state standardization | `fix: mask OIDC client secret, standardize empty states` | +| 15 | 4 (Formatting) | Number formatting utility | `fix: standardize number formatting with unit spacing` | +| 16 | 6 (Admin) | Platform label/value spacing, license badges | `fix: platform label/value spacing and badge colors` | +| 17 | 6 (Admin) | Audit log CSV export | `feat: add CSV export to audit log` | +| 18 | 2b (Interaction) | Unsaved changes banners | `fix: add unsaved changes banners to edit mode forms` | +| 19 | 7 (Polish) | Nice-to-have items | `fix: nice-to-have polish` |