# 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: `cameleer-server-app/src/main/java/com/cameleer/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 cameleer-server-app/src/main/java/com/cameleer/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 "cameleer-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 `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. - [ ] **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: `cameleer-server-app/src/main/java/com/cameleer/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` |