Rename Java packages from com.cameleer3 to com.cameleer, module directories from cameleer3-* to cameleer-*, and all references throughout workflows, Dockerfiles, docs, migrations, and pom.xml. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
57 KiB
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:
{ 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):
// 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:
// 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:
- Navigate to
https://<host>/server/deployments— should redirect to/server/apps - Navigate to Runtime > click an agent > scroll to GC Pauses chart — X-axis should show numeric labels, not ISO timestamps
- Step 4: Commit
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
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):
// BEFORE:
if (oidcEnabled) {
return ResponseEntity.badRequest().build();
}
Change to return a descriptive error:
// 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:
{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:
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
- Navigate to Admin > Users & Roles
- Click "+ Add user", select "Local" provider
- If OIDC is enabled, the info callout should appear
- Attempting to create should show the descriptive error message, not just "Failed to create user"
- Step 5: Commit
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:
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:
- Any
useNavigate()ornavigate()calls triggered by data updates - Any
useEffectthat callsnavigatebased on exchange/catalog data changes - Any auto-refresh callback that might trigger navigation
- The
sidebarRevealPathstate — what sets it?
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
useEffectthat 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:
// 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:
// 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
- Open Admin > Users & Roles
- Wait 30-60 seconds while agents are sending data
- Interact with the form (click fields, open dropdowns)
- Confirm no redirect to /server/exchanges occurs
- Step 4: Commit
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:
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:
<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:
/* 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:
/* BEFORE: */
.container {
padding: 16px;
overflow-y: auto;
flex: 1;
}
/* AFTER: */
.container {
padding: 20px 24px 40px;
overflow-y: auto;
flex: 1;
}
- Step 5: Verify visually
- Exchanges tab: table should have card wrapper with border/shadow, matching Audit Log style
- Dashboard: content should have side margins (24px), no longer flush against sidebar
- Deployments tab: spacing should match Admin pages (20px top, 24px sides)
- Step 6: Commit
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:
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:
<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:
<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
.tablestyles 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
- Navigate to Deployments tab > click an app
- Configuration sections should have card wrappers (border, shadow, background)
- Deployment table should use DataTable with card wrapper, matching other tables
- Step 6: Commit
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:
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:
<div className={`${tableStyles.tableSection} ${styles.diagramHeight}`}>
.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:
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:
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
- Dashboard: errors section and diagram section should still look the same (card styling from shared module now)
- Runtime > Agent detail: process card and timeline card should have consistent card styling
- Admin > Database: tables should have card wrappers
- Admin > Users & Roles: detail panel sections should have card backgrounds
- Admin > Environments: detail panel sections should have card backgrounds
- Step 8: Commit
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:
{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:
const [stopTarget, setStopTarget] = useState<{ id: string; name: string } | null>(null);
Replace the immediate stop with a dialog trigger:
// 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:
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
Change the delete button (around line 249) from immediate to dialog trigger:
<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:
const [killTarget, setKillTarget] = useState<string | null>(null);
<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:
const [removeRoleTarget, setRemoveRoleTarget] = useState<{ userId: string; roleId: string; roleName: string } | null>(null);
Change the onRemove to open a dialog instead of immediate mutation:
onRemove={() => setRemoveRoleTarget({ userId: selected.userId, roleId: r.id, roleName: r.name })}
Add the AlertDialog:
<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:
// 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:
<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
- AppConfigDetailPage: Edit mode shows Cancel (left) | Save (right, primary, with spinner)
- Deployments: Stop button shows type-to-confirm dialog
- TapConfigModal: Delete shows confirmation dialog
- Database: Kill shows AlertDialog with warning
- Users: Removing a role shows AlertDialog
- All Cancel buttons use ghost variant
- Step 9: Commit
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:
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):
{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:
import { PageLoader } from '../../components/PageLoader';
// ...
if (usersLoading) return <PageLoader />;
GroupsTab.tsx — same pattern:
import { PageLoader } from '../../components/PageLoader';
if (groupsLoading) return <PageLoader />;
RolesTab.tsx:
import { PageLoader } from '../../components/PageLoader';
if (rolesLoading) return <PageLoader />;
EnvironmentsPage.tsx:
import { PageLoader } from '../../components/PageLoader';
if (envsLoading) return <PageLoader />;
OidcConfigPage.tsx — currently returns null, change to:
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
- OIDC page: starts in read-only mode, Edit button enters edit mode, Cancel discards changes
- All admin pages show centered PageLoader spinner (not small inline Spinner)
- Error toasts use "Failed to [verb] [noun]" format
- Step 5: Commit
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-mutedand--text-faint) -
Modify: Multiple CSS modules (font-size changes)
-
Step 1: Find where design system tokens are defined
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):
/* 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
/* 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
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
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
- Check light mode: muted text should be noticeably darker
- Check dark mode: muted text should be clearly readable, faint text should be visible
- All labels and meta text should be at least 12px
- Run a contrast checker browser extension to verify ratios
- Step 7: Commit
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):
// 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:
// 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:
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):
// BEFORE:
render: (_: unknown, row: Row) => (
<MonoText size="xs">{row.executionId}</MonoText>
),
Change to truncated display with tooltip:
// 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
- Exchange table: IDs should show
...0001E75Cformat - Hovering should show full ID
- Clicking the ID should copy to clipboard
- Durations:
321000msshould show as5m 21s,6700msas6.7s,178msas178ms
- Step 4: Commit
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(ifstatusLabelis 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:
const hasAttributes = exchanges.some(e => e.attributes && Object.keys(e.attributes).length > 0);
In the columns array, conditionally include the Attributes column:
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:
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:
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:
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:
render: (_: unknown, row: Row) => (
<span title={row.agentId}>{shortAgentName(row.agentId)}</span>
),
- Step 4: Verify
- Exchange table: Attributes column should be hidden (all "---" currently)
- Status shows "OK"/"ERR" in both table and detail panel
- Agent names show truncated form with full name on hover
- Step 5: Commit
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:
// 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:
// 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:
// 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:
<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:
{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:
/* 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
- Agent Throughput chart: Y-axis scales to actual data range (not 1.2k when data is ~2)
- Agent Error Rate chart: shows "%" label
- Agent Memory chart: shows reference line at max heap
- Agent state: no confusing "UNKNOWN" alongside "LIVE"
- Dashboard: Application Health table rows clickable without CSS interception
- Step 7: Commit
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:
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:
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:
// 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:
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:
const [newPasswordConfirm, setNewPasswordConfirm] = useState('');
const passwordMismatch = newPassword.length > 0 && newPasswordConfirm.length > 0 && newPassword !== newPasswordConfirm;
After the existing password input:
{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:
disabled={!newUsername.trim() || (newProvider === 'local' && (!newPassword.trim() || passwordMismatch)) || duplicateUsername}
Reset newPasswordConfirm when the form closes.
Add .hintText to the CSS module:
.hintText {
font-size: 12px;
color: var(--text-muted);
}
- Step 4: Verify
- Create user form: shows password confirmation field, validation message on mismatch, policy hint
- API errors show descriptive messages in toasts
- Role descriptions show rendered characters (middle dot), not escape sequences
- Step 5: Commit
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:
const [showSecret, setShowSecret] = useState(false);
<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:
import { EmptyState } from '@cameleer/design-system';
In AppsTab.tsx, replace all <p className={styles.emptyNote}>...</p> with:
<EmptyState title="No deployments" description="Deploy this application to see deployment history." />
In GroupsTab.tsx, replace <span className={styles.inheritedNote}>(no members)</span> with:
<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
- OIDC page: client secret is masked by default, eye icon toggles visibility
- Empty states across all pages show consistent centered component
- Step 4: Commit
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:
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.
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
- KPI values show consistent formatting: "6.7 s", "1.9 %", "7.1 msg/s"
- Large numbers use K/M suffixes consistently
- Step 4: Commit
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
# 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:
<span>Slug{tenant.slug}</span>
To:
<span>Slug: {tenant.slug}</span>
Or better, use a definition list pattern:
<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:
// 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
# 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:
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:
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:
<Button variant="ghost" size="sm" onClick={() => exportCsv(events)}>
<Download size={14} /> Export CSV
</Button>
Import Download from lucide-react.
- Step 2: Verify
- Navigate to Admin > Audit Log
- Click "Export CSV" — should download a CSV file with current page data
- Open CSV and verify columns match the table
- Step 3: Commit
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:
{editing && (
<div className={styles.editBanner}>
Editing configuration. Changes are not saved until you click Save.
</div>
)}
Add the 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:
{editingResources && (
<div className={styles.editBanner}>
Editing resource defaults. Changes are not saved until you click Save.
</div>
)}
- Step 3: Verify
- AppConfigDetailPage: amber banner appears when in edit mode
- Environments: amber banner appears when editing resource defaults or JAR retention
- Step 4: Commit
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
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 |