Detailed step-by-step plan covering critical bug fixes, layout/interaction consistency, WCAG contrast compliance, data formatting, chart fixes, and admin polish. Each task includes exact file paths, code snippets, and verification steps. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1689 lines
57 KiB
Markdown
1689 lines
57 KiB
Markdown
# 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: <Navigate to="apps" replace /> },
|
|
```
|
|
|
|
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://<host>/server/deployments` — should redirect to `/server/apps`
|
|
2. Navigate to Runtime > click an agent > scroll to GC Pauses chart — X-axis should show numeric labels, not ISO timestamps
|
|
|
|
- [ ] **Step 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 && (
|
|
<InfoCallout variant="amber">
|
|
Local user creation is disabled while OIDC is enabled.
|
|
Switch to OIDC to pre-register a user, or disable OIDC first.
|
|
</InfoCallout>
|
|
)}
|
|
```
|
|
|
|
Also update the error toast handler (in `handleCreate`) to surface the API error message. Find the catch block and use the response body:
|
|
|
|
```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
|
|
<div className={tableStyles.tableSection}>
|
|
<div className={tableStyles.tableHeader}>
|
|
<span className={tableStyles.tableTitle}>Recent Exchanges</span>
|
|
<div className={tableStyles.tableRight}>
|
|
<span className={tableStyles.tableMeta}>{exchanges.length} of {formatNumber(total)} exchanges</span>
|
|
{/* existing auto-refresh indicator */}
|
|
</div>
|
|
</div>
|
|
<DataTable ... />
|
|
</div>
|
|
```
|
|
|
|
Replace the custom `.tableHeader`, `.tableTitle`, `.tableRight`, `.tableMeta` class usages with the shared module equivalents.
|
|
|
|
- [ ] **Step 2: Remove custom table classes from Dashboard.module.css**
|
|
|
|
In `ui/src/pages/Dashboard/Dashboard.module.css`, remove the custom `.tableHeader`, `.tableTitle`, `.tableRight`, `.tableMeta` classes (they're now provided by the shared module). Keep any other custom classes that aren't table-related.
|
|
|
|
- [ ] **Step 3: Add side padding to DashboardTab**
|
|
|
|
In `ui/src/pages/DashboardTab/DashboardTab.module.css`, update `.content`:
|
|
|
|
```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
|
|
<div className={sectionStyles.section}>
|
|
<SectionHeader>Monitoring</SectionHeader>
|
|
{/* existing monitoring controls: Engine Level, Payload Capture, etc. */}
|
|
</div>
|
|
```
|
|
|
|
Apply this to each sub-tab's content area. The existing `SectionHeader` components mark where sections begin — wrap each section header + its controls in `sectionStyles.section`.
|
|
|
|
- [ ] **Step 3: Replace manual `<table>` with DataTable in OverviewSubTab**
|
|
|
|
Find the manual `<table>` in `OverviewSubTab` (lines 623-680). Replace with:
|
|
|
|
```tsx
|
|
<div className={tableStyles.tableSection}>
|
|
<div className={tableStyles.tableHeader}>
|
|
<span className={tableStyles.tableTitle}>Deployments</span>
|
|
</div>
|
|
<DataTable
|
|
columns={[
|
|
{ key: 'environment', header: 'Environment', render: (_, row) => <Badge>{row.environmentSlug}</Badge> },
|
|
{ key: 'version', header: 'Version' },
|
|
{ key: 'status', header: 'Status', render: (_, row) => <Badge variant={statusVariant(row.status)}>{row.status}</Badge> },
|
|
{ key: 'deployStage', header: 'Deploy Stage' },
|
|
{ key: 'actions', header: '', render: (_, row) => (
|
|
row.status === 'RUNNING' || row.status === 'STARTING' ? (
|
|
<Button size="sm" variant="danger" onClick={() => onStop(row.id)}>Stop</Button>
|
|
) : null
|
|
)},
|
|
]}
|
|
data={deployments}
|
|
emptyMessage="No deployments yet."
|
|
/>
|
|
</div>
|
|
```
|
|
|
|
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
|
|
<div className={`${tableStyles.tableSection} ${styles.diagramHeight}`}>
|
|
```
|
|
|
|
```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:
|
|
<div className={sectionStyles.section}>
|
|
<SectionHeader>Group Membership</SectionHeader>
|
|
{/* existing membership tags */}
|
|
</div>
|
|
|
|
<div className={sectionStyles.section}>
|
|
<SectionHeader>Effective Roles</SectionHeader>
|
|
{/* existing role tags */}
|
|
</div>
|
|
```
|
|
|
|
- [ ] **Step 7: Verify visually**
|
|
|
|
1. Dashboard: errors section and diagram section should still look the same (card styling from shared module now)
|
|
2. Runtime > Agent detail: process card and timeline card should have consistent card styling
|
|
3. Admin > Database: tables should have card wrappers
|
|
4. Admin > Users & Roles: detail panel sections should have card backgrounds
|
|
5. Admin > Environments: detail panel sections should have card backgrounds
|
|
|
|
- [ ] **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 ? (
|
|
<div className={styles.toolbarActions}>
|
|
<Button variant="ghost" size="sm" onClick={cancelEditing}>Cancel</Button>
|
|
<Button variant="primary" size="sm" onClick={handleSave} loading={updateConfig.isPending}>Save</Button>
|
|
</div>
|
|
) : (
|
|
<Button variant="secondary" size="sm" onClick={startEditing}><Pencil size={14} /> Edit</Button>
|
|
)}
|
|
```
|
|
|
|
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:
|
|
<Button size="sm" variant="danger" onClick={() => setStopTarget({ id: row.id, name: app.displayName })}>Stop</Button>
|
|
|
|
// Add ConfirmDialog near the bottom of the component:
|
|
<ConfirmDialog
|
|
open={!!stopTarget}
|
|
onCancel={() => 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
|
|
<Button variant="danger" onClick={() => setShowDeleteConfirm(true)}>Delete</Button>
|
|
|
|
<ConfirmDialog
|
|
open={showDeleteConfirm}
|
|
onCancel={() => 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<string | null>(null);
|
|
```
|
|
|
|
```tsx
|
|
<Button variant="danger" size="sm" onClick={() => setKillTarget(query.pid)}>Kill</Button>
|
|
|
|
<AlertDialog
|
|
open={!!killTarget}
|
|
onCancel={() => 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
|
|
<AlertDialog
|
|
open={!!removeRoleTarget}
|
|
onCancel={() => 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:
|
|
<Button variant="secondary" onClick={onClose}>Cancel</Button>
|
|
// AFTER:
|
|
<Button variant="ghost" onClick={onClose}>Cancel</Button>
|
|
```
|
|
|
|
In `ui/src/pages/Routes/RouteDetail.tsx`, find the tap modal footer Cancel button and change `variant="secondary"` to `variant="ghost"`.
|
|
|
|
- [ ] **Step 7: Add loading prop to ConfirmDialogs that lack it**
|
|
|
|
In `ui/src/pages/Admin/OidcConfigPage.tsx` (around line 258), find the ConfirmDialog for OIDC delete. Add loading prop — track the delete operation state:
|
|
|
|
```tsx
|
|
<ConfirmDialog
|
|
...existing props...
|
|
loading={deleting} // add this prop
|
|
/>
|
|
```
|
|
|
|
In `ui/src/pages/Routes/RouteDetail.tsx` (around line 992), find the tap delete ConfirmDialog. Add `loading` prop if a mutation state is available.
|
|
|
|
- [ ] **Step 8: Verify**
|
|
|
|
1. AppConfigDetailPage: Edit mode shows Cancel (left) | Save (right, primary, with spinner)
|
|
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<OidcFormData | null>(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 ? (
|
|
<>
|
|
<Button variant="ghost" size="sm" onClick={cancelEditing}>Cancel</Button>
|
|
<Button variant="primary" size="sm" onClick={handleSave} loading={saving}>Save</Button>
|
|
</>
|
|
) : (
|
|
<>
|
|
<Button variant="secondary" size="sm" onClick={handleTestConnection}>Test Connection</Button>
|
|
<Button variant="secondary" size="sm" onClick={startEditing}>Edit</Button>
|
|
</>
|
|
)}
|
|
```
|
|
|
|
Make all form fields read-only when `!editing`. The form should display values from `form` in read mode and `formDraft` in edit mode. Update `handleSave` to use `formDraft` and call `cancelEditing` on success (after updating `form` with saved data).
|
|
|
|
Remove the inline `<Alert>` (line 138-139) — keep only the toast for errors (fixes 2b.14).
|
|
|
|
- [ ] **Step 2: Replace Spinner with PageLoader across admin pages**
|
|
|
|
In each of these files, replace bare `<Spinner>` returns with `<PageLoader />`:
|
|
|
|
**UsersTab.tsx** — find `if (usersLoading) return <Spinner size="md" />;` and replace:
|
|
```typescript
|
|
import { PageLoader } from '../../components/PageLoader';
|
|
// ...
|
|
if (usersLoading) return <PageLoader />;
|
|
```
|
|
|
|
**GroupsTab.tsx** — same pattern:
|
|
```typescript
|
|
import { PageLoader } from '../../components/PageLoader';
|
|
if (groupsLoading) return <PageLoader />;
|
|
```
|
|
|
|
**RolesTab.tsx:**
|
|
```typescript
|
|
import { PageLoader } from '../../components/PageLoader';
|
|
if (rolesLoading) return <PageLoader />;
|
|
```
|
|
|
|
**EnvironmentsPage.tsx:**
|
|
```typescript
|
|
import { PageLoader } from '../../components/PageLoader';
|
|
if (envsLoading) return <PageLoader />;
|
|
```
|
|
|
|
**OidcConfigPage.tsx** — currently returns `null`, change to:
|
|
```typescript
|
|
import { PageLoader } from '../../components/PageLoader';
|
|
if (!form) return <PageLoader />;
|
|
```
|
|
|
|
**AppsTab.tsx** (AppListView and AppDetailView) — find `<Spinner size="md" />` returns and replace with `<PageLoader />`.
|
|
|
|
- [ ] **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) => (
|
|
<MonoText size="xs">{row.executionId}</MonoText>
|
|
),
|
|
```
|
|
|
|
Change to truncated display with tooltip:
|
|
|
|
```typescript
|
|
// AFTER:
|
|
render: (_: unknown, row: Row) => (
|
|
<MonoText size="xs" title={row.executionId} style={{ cursor: 'pointer' }}
|
|
onClick={(e: React.MouseEvent) => {
|
|
e.stopPropagation();
|
|
navigator.clipboard.writeText(row.executionId);
|
|
}}>
|
|
...{row.executionId.slice(-8)}
|
|
</MonoText>
|
|
),
|
|
```
|
|
|
|
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 <span className={styles.dim}>—</span>;
|
|
return (
|
|
<span className={styles.attrBadges}>
|
|
{Object.entries(attrs).map(([k, v]) => (
|
|
<Badge key={k} size="sm">{k}={v}</Badge>
|
|
))}
|
|
</span>
|
|
);
|
|
},
|
|
}] : []),
|
|
// ...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) => (
|
|
<span title={row.agentId}>{shortAgentName(row.agentId)}</span>
|
|
),
|
|
```
|
|
|
|
- [ ] **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:
|
|
<LineChart series={errorSeries} height={160} yLabel="err/h" />
|
|
|
|
// AFTER:
|
|
<LineChart series={errorSeries} height={160} yLabel="%" />
|
|
```
|
|
|
|
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
|
|
<LineChart
|
|
series={heapSeries}
|
|
height={160}
|
|
yLabel="MB"
|
|
referenceLine={maxHeapMb} // Add reference line at max heap
|
|
referenceLabel="Max Heap"
|
|
/>
|
|
```
|
|
|
|
If the chart component doesn't support reference lines, this may need to be deferred or the component extended.
|
|
|
|
- [ ] **Step 4: Fix agent state "UNKNOWN" display**
|
|
|
|
Find where the dual state (LIVE + UNKNOWN) is displayed. In the agent detail header area, there's likely a state badge showing both the agent state and a container state.
|
|
|
|
If the secondary state is "UNKNOWN" while the primary is "LIVE", hide it:
|
|
|
|
```tsx
|
|
{agent.state && agent.state !== 'UNKNOWN' && agent.state !== agent.primaryState && (
|
|
<Badge variant="neutral">{agent.state}</Badge>
|
|
)}
|
|
```
|
|
|
|
Or add a label: `<Badge variant="neutral">Container: {agent.containerState}</Badge>`
|
|
|
|
- [ ] **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<string> {
|
|
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 <token>" https://<host>/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' && (
|
|
<>
|
|
<Input
|
|
placeholder="Password *"
|
|
type="password"
|
|
value={newPassword}
|
|
onChange={(e) => setNewPassword(e.target.value)}
|
|
/>
|
|
<Input
|
|
placeholder="Confirm Password *"
|
|
type="password"
|
|
value={newPasswordConfirm}
|
|
onChange={(e) => setNewPasswordConfirm(e.target.value)}
|
|
/>
|
|
{passwordMismatch && <span className={styles.errorText}>Passwords do not match</span>}
|
|
<span className={styles.hintText}>Min 12 characters, 3 of 4: uppercase, lowercase, number, special</span>
|
|
</>
|
|
)}
|
|
```
|
|
|
|
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
|
|
<div style={{ position: 'relative' }}>
|
|
<Input
|
|
type={showSecret ? 'text' : 'password'}
|
|
value={formDraft?.clientSecret ?? form?.clientSecret ?? ''}
|
|
onChange={(e) => updateField('clientSecret', e.target.value)}
|
|
readOnly={!editing}
|
|
/>
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
style={{ position: 'absolute', right: 4, top: '50%', transform: 'translateY(-50%)' }}
|
|
onClick={() => setShowSecret(!showSecret)}
|
|
>
|
|
{showSecret ? <EyeOff size={14} /> : <Eye size={14} />}
|
|
</Button>
|
|
</div>
|
|
```
|
|
|
|
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 `<p className={styles.emptyNote}>...</p>` with:
|
|
```tsx
|
|
<EmptyState title="No deployments" description="Deploy this application to see deployment history." />
|
|
```
|
|
|
|
In `GroupsTab.tsx`, replace `<span className={styles.inheritedNote}>(no members)</span>` with:
|
|
```tsx
|
|
<EmptyState title="No members" description="Add members to this group." />
|
|
```
|
|
|
|
In `RouteDetail.tsx`, replace `<div className={styles.emptyText}>...</div>` and `<div className={styles.emptyState}>...</div>` with `EmptyState` component.
|
|
|
|
Use the same pattern everywhere: `<EmptyState title="No X" description="..." />`
|
|
|
|
- [ ] **Step 3: Verify**
|
|
|
|
1. OIDC page: client secret is masked by default, eye icon toggles visibility
|
|
2. Empty states across all pages show consistent centered component
|
|
|
|
- [ ] **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
|
|
<span>Slug{tenant.slug}</span>
|
|
```
|
|
To:
|
|
```tsx
|
|
<span>Slug: {tenant.slug}</span>
|
|
```
|
|
|
|
Or better, use a definition list pattern:
|
|
```tsx
|
|
<dl className={styles.meta}>
|
|
<dt>Slug</dt><dd>{tenant.slug}</dd>
|
|
<dt>Status</dt><dd><Badge>{tenant.status}</Badge></dd>
|
|
<dt>Created</dt><dd>{formatDate(tenant.createdAt)}</dd>
|
|
</dl>
|
|
```
|
|
|
|
- [ ] **Step 3: Fix license badge colors**
|
|
|
|
Find the license features display. Change DISABLED badges from error/red to neutral:
|
|
|
|
```tsx
|
|
// BEFORE:
|
|
<Badge variant={feature.enabled ? 'success' : 'error'}>{feature.enabled ? 'ENABLED' : 'DISABLED'}</Badge>
|
|
|
|
// AFTER:
|
|
<Badge variant={feature.enabled ? 'success' : 'neutral'}>{feature.enabled ? 'ENABLED' : 'NOT INCLUDED'}</Badge>
|
|
```
|
|
|
|
- [ ] **Step 4: Commit**
|
|
|
|
```bash
|
|
# If in this repo:
|
|
git add <platform-ui-files>
|
|
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
|
|
<Button variant="ghost" size="sm" onClick={() => exportCsv(events)}>
|
|
<Download size={14} /> Export CSV
|
|
</Button>
|
|
```
|
|
|
|
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 && (
|
|
<div className={styles.editBanner}>
|
|
Editing configuration. Changes are not saved until you click Save.
|
|
</div>
|
|
)}
|
|
```
|
|
|
|
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 && (
|
|
<div className={styles.editBanner}>
|
|
Editing resource defaults. Changes are not saved until you click Save.
|
|
</div>
|
|
)}
|
|
```
|
|
|
|
- [ ] **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` |
|