Rename Java packages from net.siegeln.cameleer3 to net.siegeln.cameleer, update all references in workflows, Docker configs, docs, and bootstrap. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
14 KiB
SaaS Platform UX Polish — Design Spec
Date: 2026-04-09 Scope: Bug fixes, design consistency, error handling, component quality for the cameleer-saas platform UI Out of scope: Mobile responsiveness (deferred), new features (billing, team management), admin tenant creation (#37)
Context
Playwright-driven audit of the live SaaS platform (22 screenshots) plus source code audit of ui/src/ (3 pages, sign-in page, layout, auth components). The platform has only 3 pages — Dashboard, License, Admin Tenants — plus a custom Logto sign-in page. Issues are concentrated and structural.
Audit artifacts in audit/:
platform-ui-findings.md— 30 issues from live UI auditsource-code-findings.md— 22 issues from source code analysis
Implementation Strategy
4 batches, ordered by impact. Smaller scope than the server UI polish (~25 items vs ~52).
Batch 1: Layout Fixes
Effort: 0.5 days
1.1 Fix Label/Value Collision
Problem: Throughout Dashboard and License pages, labels and values run together: "Slugdefault", "Max Agents3", "Issued8. April 2026". The code uses flex justify-between but the flex container doesn't stretch to full Card width.
Root cause (source audit): The <div className="flex justify-between"> elements are inside Card components. If the Card's inner container doesn't apply w-full or the flex children don't have enough space, justify-between collapses.
Fix: Ensure the container divs inside Cards have w-full (or className="flex justify-between w-full"). Check all label/value rows in:
DashboardPage.tsx:95-112— Tenant Information sectionLicensePage.tsx:94-115— Validity sectionLicensePage.tsx:145-158— Limits section
If the Card component's children wrapper is the constraint, wrap the content in <div className="w-full space-y-2">.
Files: ui/src/pages/DashboardPage.tsx, ui/src/pages/LicensePage.tsx
1.2 Replace Hardcoded text-white with DS Variables
Problem: Every page uses Tailwind text-white, text-white/60, text-white/80, bg-white/5, border-white/10 instead of DS CSS variables. This breaks light theme (TopBar has a working theme toggle).
Fix: Replace all hardcoded color classes with DS CSS variable equivalents using inline styles or a CSS module:
| Tailwind class | DS variable |
|---|---|
text-white |
var(--text-primary) |
text-white/80 |
var(--text-secondary) |
text-white/60 |
var(--text-muted) |
text-white/40 |
var(--text-faint) |
bg-white/5 |
var(--bg-hover) |
bg-white/10 |
var(--bg-inset) |
border-white/10 |
var(--border-subtle) |
divide-white/10 |
var(--border-subtle) |
Approach: Create a shared CSS module (ui/src/styles/platform.module.css) with classes mapping to DS variables, or switch to inline style={{ color: 'var(--text-primary)' }}. The sign-in page already demonstrates the correct pattern with CSS modules + DS variables.
Files: ui/src/pages/DashboardPage.tsx, ui/src/pages/LicensePage.tsx, ui/src/pages/AdminTenantsPage.tsx
1.3 Reduce Redundant Dashboard Content
Problem: "Open Server Dashboard" appears 3 times. Status "ACTIVE" appears 3 times. Tier badge appears 2 times.
Fix:
- Remove the "Open Server Dashboard" primary button from the header area (keep the Server Management card + sidebar footer link — 2 locations max)
- Remove the status badge from the header area (keep KPI strip + Tenant Information)
- The tier badge next to the heading is fine (quick context)
Files: ui/src/pages/DashboardPage.tsx
Batch 2: Header & Navigation
Effort: 1 day
2.1 Hide Server Controls on Platform Pages
Problem: TopBar always renders status filters (OK/Warn/Error/Running), time range pills (1h-7d), auto-refresh toggle, and command palette search. All are irrelevant on platform pages.
Fix options (pick one):
Option A (recommended): Use TopBar props to hide sections.
Check if the DS TopBar component accepts props to control which sections render. If it has showFilters, showTimeRange, showAutoRefresh, showSearch props — set them all to false in Layout.tsx.
Option B: Remove providers that feed the controls.
Don't wrap the platform app in GlobalFilterProvider and CommandPaletteProvider (in main.tsx). This may cause runtime errors if TopBar assumes they exist — test carefully.
Option C: Custom simplified header.
Replace TopBar with a simpler platform-specific header that only renders: breadcrumb, theme toggle, user menu. Use DS primitives (Breadcrumb, Avatar, Dropdown, Button) to compose it.
Investigate which option is viable by checking the DS TopBar component API.
Files: ui/src/components/Layout.tsx, possibly ui/src/main.tsx
2.2 Fix Sidebar Active State
Problem: Sidebar.Section used as navigation links via onToggle hack. No active prop set. Users can't tell which page they're on.
Fix: Pass active={true} to the current page's Sidebar.Section based on the route:
const location = useLocation();
const isActive = (path: string) => location.pathname === path || location.pathname === path + '/';
<Sidebar.Section
icon={<DashboardIcon />}
label="Dashboard"
open={false}
active={isActive('/') || isActive('/platform')}
onToggle={() => navigate('/')}
>
{null}
</Sidebar.Section>
<Sidebar.Section
icon={<LicenseIcon />}
label="License"
open={false}
active={isActive('/license')}
onToggle={() => navigate('/license')}
>
{null}
</Sidebar.Section>
Check the DS Sidebar.Section props — if active doesn't exist on Section, check if there's a Sidebar.Link or Sidebar.NavItem component that supports it.
Files: ui/src/components/Layout.tsx
2.3 Add Breadcrumbs
Problem: breadcrumb={[]} is always empty.
Fix: Set breadcrumbs per page:
// Layout.tsx:
const location = useLocation();
const breadcrumb = useMemo(() => {
if (location.pathname.includes('/license')) return [{ label: 'License' }];
if (location.pathname.includes('/admin')) return [{ label: 'Admin' }, { label: 'Tenants' }];
return [{ label: 'Dashboard' }];
}, [location.pathname]);
<TopBar breadcrumb={breadcrumb} ... />
Check the DS TopBar breadcrumb prop type to match the expected shape.
Files: ui/src/components/Layout.tsx
2.4 Remove One "Open Server Dashboard" Button
Problem: 3 locations for the same action.
Fix: Keep:
- Sidebar footer link (always accessible)
- Server Management card on Dashboard (contextual with description)
Remove: The primary "Open Server Dashboard" button in the header area of DashboardPage (line ~81-87).
Files: ui/src/pages/DashboardPage.tsx
2.5 Fix Sidebar Collapse
Problem: collapsed={false} hardcoded, onCollapseToggle is no-op.
Fix: Add state:
const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
<Sidebar collapsed={sidebarCollapsed} onCollapseToggle={() => setSidebarCollapsed(c => !c)}>
Files: ui/src/components/Layout.tsx
Batch 3: Error Handling & Components
Effort: 1 day
3.1 OrgResolver Error State
Problem: Returns null on error — blank screen with sidebar/TopBar but no content.
Fix: Replace return null with an error display:
if (isError) return (
<EmptyState
title="Unable to load account"
description="Failed to retrieve your organization. Please try again."
action={<Button onClick={() => refetch()}>Retry</Button>}
/>
);
Import EmptyState and Button from DS.
Files: ui/src/auth/OrgResolver.tsx
3.2 DashboardPage Error Handling
Problem: No isError check. Silently renders with - fallback values.
Fix: Add error state similar to LicensePage:
if (tenantError || licenseError) return (
<EmptyState
title="Unable to load dashboard"
description="Failed to retrieve tenant information. Please try again."
/>
);
Files: ui/src/pages/DashboardPage.tsx
3.3 Replace Raw HTML with DS Components
Problem: LicensePage uses raw <button> and <code> where DS components exist.
Fix:
Replace raw button (line ~166-170):
// BEFORE:
<button type="button" className="text-sm text-primary-400 ...">Show token</button>
// AFTER:
<Button variant="ghost" size="sm" onClick={() => setTokenExpanded(v => !v)}>
{tokenExpanded ? 'Hide token' : 'Show token'}
</Button>
Replace raw code block (line ~174-178) with DS CodeBlock if available, or at minimum use DS CSS variables instead of hardcoded Tailwind colors.
Files: ui/src/pages/LicensePage.tsx
3.4 Add Copy-to-Clipboard for License Token
Problem: Users must manually select and copy the token.
Fix: Add a copy button next to the token:
<Button variant="ghost" size="sm" onClick={() => {
navigator.clipboard.writeText(license.token);
toast({ title: 'Token copied', variant: 'success' });
}}>
<Copy size={14} /> Copy
</Button>
Import Copy from lucide-react and useToast from DS.
Files: ui/src/pages/LicensePage.tsx
3.5 Fix Username Null = No Logout
Problem: When username is null, no user indicator or logout button appears.
Fix: Always pass a user object to TopBar — fallback to email or "User":
const displayName = username || user?.email || 'User';
<TopBar user={{ name: displayName }} onLogout={logout} />
Files: ui/src/components/Layout.tsx
3.6 Add Password Visibility Toggle to Sign-In
Problem: No eye icon to reveal password.
Fix: The DS Input component may support a type toggle. If not, wrap with a show/hide toggle:
const [showPassword, setShowPassword] = useState(false);
<div style={{ position: 'relative' }}>
<Input type={showPassword ? 'text' : 'password'} ... />
<Button variant="ghost" size="sm"
style={{ position: 'absolute', right: 4, top: '50%', transform: 'translateY(-50%)' }}
onClick={() => setShowPassword(!showPassword)}>
{showPassword ? <EyeOff size={14} /> : <Eye size={14} />}
</Button>
</div>
Files: ui/sign-in/src/SignInPage.tsx
3.7 Admin Page Error Fallback
Problem: /platform/admin/tenants returns HTTP error with no graceful fallback.
Fix: Add error boundary or error state in AdminTenantsPage:
if (isError) return (
<EmptyState
title="Unable to load tenants"
description="You may not have admin permissions, or the server is unavailable."
/>
);
Files: ui/src/pages/AdminTenantsPage.tsx
Batch 4: Polish
Effort: 0.5 days
4.1 Unify tierColor() Mapping
Problem: Defined twice with different tier names:
DashboardPage.tsx:12-18maps enterprise/pro/starterLicensePage.tsx:25-33maps BUSINESS/HIGH/MID/LOW
Fix: Extract a single tierColor() to a shared utility (ui/src/utils/tier.ts). Map all known tier names:
export function tierColor(tier: string): BadgeColor {
switch (tier?.toUpperCase()) {
case 'BUSINESS': case 'ENTERPRISE': return 'success';
case 'HIGH': case 'PRO': return 'primary';
case 'MID': case 'STARTER': return 'warning';
case 'LOW': case 'FREE': return 'auto';
default: return 'auto';
}
}
Import from both pages.
Files: New ui/src/utils/tier.ts, modify DashboardPage.tsx, LicensePage.tsx
4.2 Fix Feature Badge Colors
Problem: Disabled features use color="auto" (hash-based, inconsistent). Should use muted neutral.
Fix: Check if DS Badge supports a neutral or default color variant. If not, use the closest muted option. The goal: enabled = green success, disabled = gray/muted (not red, not random).
Files: ui/src/pages/LicensePage.tsx
4.3 AdminTenantsPage Improvements
Problem: Row click silently switches tenant context. createdAt renders raw ISO. No empty state.
Fix:
- Add confirmation before tenant switch:
if (!confirm('Switch to tenant "X"?')) return;(or DS AlertDialog) - Format date:
new Date(row.createdAt).toLocaleDateString() - Add empty state:
<EmptyState title="No tenants" description="Create a tenant to get started." />
Files: ui/src/pages/AdminTenantsPage.tsx
4.4 Replace Custom SVG Icons with Lucide
Problem: Layout.tsx has 4 inline SVG icon components instead of using lucide-react.
Fix: Replace with lucide icons:
DashboardIcon-><LayoutDashboard size={18} />LicenseIcon-><ShieldCheck size={18} />PlatformIcon-><Building size={18} />ServerIcon-><Server size={18} />
Import from lucide-react.
Files: ui/src/components/Layout.tsx
4.5 Sign-In Branding
Problem: Login says "cameleer" — internal repo name, not product brand.
Fix: Change to "Cameleer" (product name). Update the page title from "Sign in — cameleer" to "Sign in — Cameleer".
Files: ui/sign-in/src/SignInPage.tsx
Implementation Order
| Order | Batch | Items | Effort |
|---|---|---|---|
| 1 | Batch 1: Layout Fixes | 3 | 0.5 days |
| 2 | Batch 2: Header & Navigation | 5 | 1 day |
| 3 | Batch 3: Error Handling & Components | 7 | 1 day |
| 4 | Batch 4: Polish | 5 | 0.5 days |
Total: ~20 items across 4 batches, ~3 days of work.
Related Issues
| Issue | Relevance |
|---|---|
| #1 | Epic: SaaS Management Platform — this spec covers polish only |
| #37 | Admin: Tenant creation UI — not covered (feature work) |
| #38 | Cross-app session management — not covered (parked) |
Out of Scope
- Mobile responsiveness (deferred per user request)
- New features (billing, team management, tenant creation)
- Admin tenant CRUD workflow (#37)
- Cross-app session sync (#38)