Add SaaS platform UX polish design spec with audit findings
Playwright audit (22 screenshots) + source code audit covering all platform pages. Spec defines 4 batches: layout fixes (label/value collision, hardcoded colors), header/navigation (hide server controls, sidebar active state), error handling & components (DS adoption, copy-to-clipboard, error states), and polish (tier colors, badges). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
269
audit/platform-ui-findings.md
Normal file
269
audit/platform-ui-findings.md
Normal file
@@ -0,0 +1,269 @@
|
||||
# Cameleer SaaS Platform UI Audit Findings
|
||||
|
||||
**Date:** 2026-04-09
|
||||
**Auditor:** Claude Opus 4.6
|
||||
**URL:** https://desktop-fb5vgj9.siegeln.internal/
|
||||
**Credentials:** admin/admin
|
||||
**Browser:** Playwright (Chromium)
|
||||
|
||||
---
|
||||
|
||||
## 1. Login Page (`/sign-in`)
|
||||
|
||||
**Screenshot:** `03-login-page.png`, `04-login-error.png`
|
||||
|
||||
### What works well
|
||||
- Clean, centered card layout with consistent design system components
|
||||
- Fun rotating subtitle taglines (e.g., "No ticket, no caravan") add personality
|
||||
- Cameleer logo is displayed correctly
|
||||
- Error handling works -- "Invalid username or password" alert appears on bad credentials (red alert banner)
|
||||
- Sign in button is correctly disabled until both fields are populated
|
||||
- Loading state on button during authentication
|
||||
- Uses proper `autoComplete` attributes (`username`, `current-password`)
|
||||
|
||||
### Issues found
|
||||
|
||||
| Severity | Issue | Element |
|
||||
|----------|-------|---------|
|
||||
| Important | **No password visibility toggle** -- the Password input uses `type="password"` with no eye icon to reveal. Most modern login forms offer this. | Password field |
|
||||
| Important | **Branding says "cameleer3"** not "Cameleer" or "Cameleer SaaS" -- the product name on the login page is the internal repo name, not the user-facing brand | `.logo` text content |
|
||||
| Nice-to-have | **No "Forgot password" link** -- even if it goes to a "contact admin" page, users expect this | Below password field |
|
||||
| Nice-to-have | **No Enter-key submit hint** -- though Enter does work via form submit, there's no visual affordance | Form area |
|
||||
| Nice-to-have | **Page title is "Sign in -- cameleer3"** -- should match product branding ("Cameleer SaaS") | `<title>` tag |
|
||||
|
||||
---
|
||||
|
||||
## 2. Platform Dashboard (`/platform/`)
|
||||
|
||||
**Screenshots:** `05-platform-dashboard-loggedin.png`, `15-dashboard-desktop-1280.png`, `19-tenant-info-detail.png`, `20-kpi-strip-detail.png`
|
||||
|
||||
### What works well
|
||||
- Clear tenant name as page heading ("Example Tenant")
|
||||
- Tier badge next to tenant name provides immediate context
|
||||
- KPI strip with Tier, Status, License cards is visually clean and well-structured
|
||||
- License KPI card shows expiry date in green "expires 8.4.2027" trend indicator
|
||||
- "Server Management" card provides clear description of what the server dashboard does
|
||||
|
||||
### Issues found
|
||||
|
||||
| Severity | Issue | Element |
|
||||
|----------|-------|---------|
|
||||
| **Critical** | **Label/value collision in Tenant Information card** -- "Slugdefault", "Created8.4.2026" have no visual separation between label and value. The source uses `flex justify-between` but the deployed Card component doesn't give the inner `div` full width, so items stack/collapse. | Tenant Information card |
|
||||
| **Critical** | **"Open Server Dashboard" appears 3 times** on one page: (1) primary button in header area below tenant name, (2) "Server Management" card with secondary button, (3) sidebar footer link. This is redundant and clutters the page. Reduce to 1-2 locations max. | Header area, Server Management card, sidebar footer |
|
||||
| Important | **Breadcrumb is always empty** -- the `breadcrumb` prop is passed as `[]`. Platform pages should have breadcrumbs like "Platform > Dashboard" or "Platform > License". | TopBar breadcrumb nav |
|
||||
| Important | **Massive empty space below content** -- the dashboard only has ~4 cards but the page extends far below with blank white/cream space. The page feels sparse and "stub-like." | Below Server Management card |
|
||||
| Important | **Tier badge color is misleading** -- "LOW" tier uses `primary` (orange) color, which doesn't convey it's the lowest/cheapest tier. The `tierColor()` function in DashboardPage maps to enterprise=success, pro=primary, starter=warning, but the actual data uses LOW/MID/HIGH/BUSINESS tiers (defined in LicensePage). Dashboard and License pages have different tier color mappings. | Tier badge |
|
||||
| Important | **Status is shown redundantly** -- "ACTIVE" appears in (1) KPI strip Status card, (2) Tenant Information card with badge, and (3) header area badge. This is excessive for a single piece of information. | Multiple locations |
|
||||
| Nice-to-have | **No tenant ID/slug in breadcrumb or subtitle** -- the slug "default" only appears buried in the Tenant Information card | Page header area |
|
||||
|
||||
---
|
||||
|
||||
## 3. License Page (`/platform/license`)
|
||||
|
||||
**Screenshots:** `06-license-page.png`, `07-license-token-revealed.png`, `16-license-features-detail.png`, `17-license-limits-detail.png`, `18-license-validity-detail.png`
|
||||
|
||||
### What works well
|
||||
- Well-structured layout with logical sections (Validity, Features, Limits, License Token)
|
||||
- Tier badge in header provides context
|
||||
- Feature matrix clearly shows enabled vs disabled features
|
||||
- "Days remaining" with color-coded badge (green for healthy, warning for <30 days, red for expired)
|
||||
- Token show/hide toggle works correctly
|
||||
- Token revealed in monospace code block with appropriate styling
|
||||
|
||||
### Issues found
|
||||
|
||||
| Severity | Issue | Element |
|
||||
|----------|-------|---------|
|
||||
| **Critical** | **Label/value collision in Validity section** -- "Issued8. April 2026" and "Expires8. April 2027" have no separation. Source code uses `flex items-center justify-between` but the flex container seems to not be stretching to full width. | Validity card rows |
|
||||
| **Critical** | **Label/value collision in Limits section** -- "Max Agents3", "Retention Days7", "Max Environments1" have labels and values mashed together. Source uses `flex items-center justify-between` layout but the same rendering bug prevents proper spacing. | Limits card rows |
|
||||
| Important | **No "Copy to clipboard" button** for the license token -- users need to manually select and copy. A copy button with confirmation toast is standard UX for tokens/secrets. | License Token section |
|
||||
| Important | **Feature badge text mismatch** -- Source code says `'Not included'` for disabled features, but deployed version shows "DISABLED". This suggests the deployed build is out of sync with the source. | Features card badges |
|
||||
| Important | **"Disabled" badge color** -- disabled features use `color='auto'` (which renders as a neutral/red-ish badge), while "Enabled" uses green. Consider using a muted gray for "Not included" to make it feel less like an error state. Red implies something is wrong, but a feature simply not being in the plan is not an error. | Features card disabled badges |
|
||||
| Nice-to-have | **Limits values are not right-aligned** -- due to the label/value collision, the numeric values don't align in a column, making comparison harder | Limits card |
|
||||
| Nice-to-have | **No units on limits** -- "Retention Days7" should be "7 days", "Max Agents3" should be "3 agents" or just "3" with clear formatting | Limits card values |
|
||||
|
||||
---
|
||||
|
||||
## 4. Admin Pages (`/platform/admin/tenants`)
|
||||
|
||||
**No screenshot available -- page returns HTTP error**
|
||||
|
||||
### Issues found
|
||||
|
||||
| Severity | Issue | Element |
|
||||
|----------|-------|---------|
|
||||
| **Critical** | **Admin page returns HTTP error (net::ERR_HTTP_RESPONSE_CODE_FAILURE)** -- navigating to `/platform/admin/tenants` fails with an HTTP error. The route exists in the router (`AdminTenantsPage`), but the admin section is not visible in the sidebar (no "Platform" item shown). | Admin route |
|
||||
| Important | **Admin section not visible in sidebar** -- the `platform:admin` scope check in Layout.tsx hides the "Platform" sidebar item. Even though the user is "admin", they apparently don't have the `platform:admin` scope in their JWT. This may be intentional (scope not assigned) or a bug. | Sidebar Platform section |
|
||||
| Important | **No graceful fallback for unauthorized admin access** -- if a user manually navigates to `/admin/tenants` without the scope, the page should show a "Not authorized" message rather than an HTTP error. | Admin route error handling |
|
||||
|
||||
---
|
||||
|
||||
## 5. Navigation
|
||||
|
||||
**Screenshots:** `21-sidebar-detail.png`, `12-sidebar-collapsed.png`
|
||||
|
||||
### What works well
|
||||
- Clean sidebar with Cameleer SaaS branding and logo
|
||||
- "Open Server Dashboard" in sidebar footer is a good location
|
||||
- Sidebar has only 2 navigation items (Dashboard, License) which keeps it simple
|
||||
|
||||
### Issues found
|
||||
|
||||
| Severity | Issue | Element |
|
||||
|----------|-------|---------|
|
||||
| **Critical** | **No active state on sidebar navigation items** -- when on the Dashboard page, neither Dashboard nor License is highlighted/active. The sidebar uses `Sidebar.Section` components with `open={false}` as navigation links via `onToggle`, but `Section` is designed for expandable/collapsible groups, not navigation links. There is no visual indicator of the current page. | Sidebar items |
|
||||
| Important | **Sidebar collapse doesn't work visually** -- clicking "Collapse sidebar" toggles the `active` state on the button but the sidebar doesn't visually collapse. The Layout component passes `collapsed={false}` as a hardcoded prop and `onCollapseToggle={() => {}}` as a no-op. | Sidebar collapse button |
|
||||
| Important | **No clear distinction between "platform" and "server" levels** -- there's nothing in the sidebar header that says "Platform" vs "Server". The sidebar says "Cameleer SaaS" but when you switch to the server dashboard, it becomes a completely different app. A user might not understand the relationship. | Sidebar header |
|
||||
| Nice-to-have | **"Open Server Dashboard" opens in new tab** -- `window.open('/server/', '_blank', 'noopener')` is used. While reasonable, there's no visual indicator (external link icon) that it will open a new tab. | Sidebar footer link, dashboard buttons |
|
||||
|
||||
---
|
||||
|
||||
## 6. Header Bar (TopBar)
|
||||
|
||||
**Screenshot:** `22-header-bar-detail.png`
|
||||
|
||||
### Issues found
|
||||
|
||||
| Severity | Issue | Element |
|
||||
|----------|-------|---------|
|
||||
| **Critical** | **Server-specific controls shown on platform pages** -- the TopBar always renders: (1) Search (Ctrl+K), (2) Status filters (OK/Warn/Error/Running), (3) Time range pills (1h/3h/6h/Today/24h/7d), (4) Auto-refresh toggle (MANUAL/AUTO). None of these are relevant to the platform dashboard or license page. They are observability controls designed for the server's exchange/route monitoring. | Entire TopBar filter area |
|
||||
| Important | **Search button does nothing** -- clicking "Search..." on the platform does not open a search modal. The CommandPaletteProvider is likely not configured for the platform context. | Search button |
|
||||
| Important | **Status filter buttons are interactive but meaningless** -- clicking OK/Warn/Error/Running on platform pages toggles state (global filter provider) but has no effect on the displayed content. | Status filter buttons |
|
||||
| Important | **Time range selector is interactive but meaningless** -- similarly, changing the time range from 1h to 7d has no effect on platform pages. | Time range pills |
|
||||
| Important | **Auto-refresh toggle is misleading** -- shows "MANUAL" toggle on platform pages where there's nothing to auto-refresh. | Auto-refresh button |
|
||||
|
||||
---
|
||||
|
||||
## 7. User Menu
|
||||
|
||||
**Screenshot:** `02-user-menu-dropdown.png`
|
||||
|
||||
### What works well
|
||||
- User name "admin" and avatar initials "AD" displayed correctly
|
||||
- Dropdown appears on click with Logout option
|
||||
|
||||
### Issues found
|
||||
|
||||
| Severity | Issue | Element |
|
||||
|----------|-------|---------|
|
||||
| Important | **User menu only has "Logout"** -- there's no "Profile", "Settings", "About", or "Switch Tenant" option. For a SaaS platform, users should at minimum see their role and tenant context. | User dropdown menu |
|
||||
| Nice-to-have | **Avatar shows "AD" for "admin"** -- the Avatar component appears to use first 2 characters of the name. For "admin" this produces "AD" which looks like initials for a different name. | Avatar component |
|
||||
|
||||
---
|
||||
|
||||
## 8. Dark Mode
|
||||
|
||||
**Screenshots:** `08-dashboard-dark-mode.png`, `09-license-dark-mode.png`
|
||||
|
||||
### What works well
|
||||
- Dark mode toggle works and applies globally
|
||||
- Background transitions to dark brown/charcoal
|
||||
- Text colors adapt appropriately
|
||||
- Cards maintain visual distinction from background
|
||||
- Design system tokens handle the switch smoothly
|
||||
|
||||
### Issues found
|
||||
|
||||
| Severity | Issue | Element |
|
||||
|----------|-------|---------|
|
||||
| Nice-to-have | **Dark mode is warm-toned (brown)** rather than the more common cool dark gray/charcoal. This is consistent with the design system's cameleer branding but may feel unusual to users accustomed to dark mode in other apps. | Global dark theme |
|
||||
| Nice-to-have | **The same label/value collision issues appear in dark mode** -- these are layout bugs, not color bugs, so dark mode doesn't help or hurt. | Card content |
|
||||
|
||||
---
|
||||
|
||||
## 9. Responsiveness
|
||||
|
||||
**Screenshots:** `13-responsive-tablet.png`, `14-responsive-mobile.png`
|
||||
|
||||
### Issues found
|
||||
|
||||
| Severity | Issue | Element |
|
||||
|----------|-------|---------|
|
||||
| **Critical** | **Mobile layout is broken** -- at 375px width, the sidebar overlaps the main content. The KPI strip cards are truncated ("LO...", "AC..."). The header bar overflows. Content is unreadable. | Full page at mobile widths |
|
||||
| Important | **Tablet layout (768px) is functional but crowded** -- sidebar takes significant width, header bar items are compressed ("Se..." for Search), but content is readable. KPI strip wraps correctly. | Full page at tablet widths |
|
||||
| Important | **Sidebar doesn't collapse on mobile** -- there's no hamburger menu or responsive sidebar behavior. The sidebar is always visible, eating screen space on narrow viewports. | Sidebar |
|
||||
|
||||
---
|
||||
|
||||
## 10. Cross-cutting Concerns
|
||||
|
||||
### Loading States
|
||||
- Dashboard and License pages both show a centered `Spinner` during loading -- this works well.
|
||||
- `EmptyState` component used for "No tenant associated" and "License unavailable" -- good error handling in components.
|
||||
|
||||
### Error States
|
||||
- Login page error handling is good (alert banner)
|
||||
- No visible error boundary for unexpected errors on platform pages
|
||||
- Admin route fails silently with HTTP error -- no user-facing error message
|
||||
|
||||
### Toast Notifications
|
||||
- No toast notifications observed during the audit
|
||||
- License token copy should trigger a toast confirmation (if a copy button existed)
|
||||
|
||||
### Confirmation Dialogs
|
||||
- No destructive actions available on the platform (no delete/deactivate buttons) so no confirmation dialogs needed currently
|
||||
|
||||
---
|
||||
|
||||
## Summary of Issues by Severity
|
||||
|
||||
### Critical (5)
|
||||
1. **Label/value collision** throughout Tenant Information card, License Validity, and License Limits sections -- labels and values run together without spacing
|
||||
2. **"Open Server Dashboard" appears 3 times** on the dashboard page -- excessive redundancy
|
||||
3. **No active state on sidebar navigation items** -- users can't tell which page they're on
|
||||
4. **Server-specific header controls shown on platform pages** -- search, status filters, time range, auto-refresh are all meaningless on platform pages
|
||||
5. **Mobile layout completely broken** -- sidebar overlaps content, content truncated
|
||||
|
||||
### Important (17)
|
||||
1. No password visibility toggle on login
|
||||
2. Branding says "cameleer3" instead of product name on login
|
||||
3. Breadcrumbs always empty on platform pages
|
||||
4. Massive empty space below dashboard content
|
||||
5. Tier badge color mapping inconsistent between Dashboard and License pages
|
||||
6. Status shown redundantly in 3 places on dashboard
|
||||
7. No clipboard copy button for license token
|
||||
8. Feature badge text mismatch between source and deployed build
|
||||
9. "Disabled" badge uses red-ish color (implies error, not "not in plan")
|
||||
10. Admin page returns HTTP error with no graceful fallback
|
||||
11. Admin section invisible in sidebar despite being admin user
|
||||
12. Sidebar collapse button doesn't work (no-op handler)
|
||||
13. No clear platform vs server level distinction
|
||||
14. Search button does nothing on platform
|
||||
15. Status filters and time range interactive but meaningless on platform
|
||||
16. User menu only has Logout (no profile/settings)
|
||||
17. Sidebar doesn't collapse/hide on mobile
|
||||
|
||||
### Nice-to-have (8)
|
||||
1. No "Forgot password" link on login
|
||||
2. Login page title uses "cameleer3" branding
|
||||
3. No external link icon on "Open Server Dashboard"
|
||||
4. Avatar shows "AD" for "admin"
|
||||
5. No units on limit values
|
||||
6. Dark mode warm-toned (not standard cool dark)
|
||||
7. No Enter-key submit hint
|
||||
8. No tenant ID in breadcrumb/subtitle
|
||||
|
||||
---
|
||||
|
||||
## Overarching Assessment
|
||||
|
||||
The platform UI currently feels like a **thin shell** around the server dashboard. It has only 2 functioning pages (Dashboard and License), and both suffer from the same fundamental layout bug (label/value collision in Card components). The header bar is entirely borrowed from the server observability UI without any platform-specific adaptation, making 70% of the header controls irrelevant.
|
||||
|
||||
**Key architectural concerns:**
|
||||
1. The TopBar component from the design system is monolithic -- it always renders server-specific controls (status filters, time range, search). The platform needs either a simplified TopBar variant or the ability to hide these sections.
|
||||
2. The sidebar uses `Sidebar.Section` (expandable groups) as navigation links, which prevents active-state highlighting. It should use `Sidebar.Link` or a similar component.
|
||||
3. The platform provides very little actionable functionality -- a user can view their tenant info and license, but can't manage anything. The "Server Management" card is just a link to another app.
|
||||
|
||||
**What works well overall:**
|
||||
- Design system integration is solid (same look and feel as server)
|
||||
- Dark mode works correctly
|
||||
- Loading and error states are handled
|
||||
- Login page is clean and functional
|
||||
- KPI strip component is effective at summarizing key info
|
||||
|
||||
**Recommended priorities:**
|
||||
1. Fix the label/value collision bug (affects 3 cards across 2 pages)
|
||||
2. Hide or replace server-specific header controls on platform pages
|
||||
3. Add sidebar active state and fix the collapse behavior
|
||||
4. Add clipboard copy for license token
|
||||
5. Fix mobile responsiveness
|
||||
433
audit/source-code-findings.md
Normal file
433
audit/source-code-findings.md
Normal file
@@ -0,0 +1,433 @@
|
||||
# Cameleer SaaS UI — Source Code Audit Findings
|
||||
|
||||
**Audit date:** 2026-04-09
|
||||
**Scope:** `ui/src/` (platform SPA) + `ui/sign-in/src/` (custom Logto sign-in)
|
||||
**Design system:** `@cameleer/design-system@0.1.38`
|
||||
|
||||
---
|
||||
|
||||
## 1. Layout and Styling Patterns
|
||||
|
||||
### 1.1 Container Padding/Margin
|
||||
|
||||
All three page components use an identical outer wrapper pattern:
|
||||
|
||||
```tsx
|
||||
// DashboardPage.tsx:67, LicensePage.tsx:82, AdminTenantsPage.tsx:60
|
||||
<div className="space-y-6 p-6">
|
||||
```
|
||||
|
||||
**Verdict:** Consistent across all pages. However, this padding is applied by each page individually rather than by the `Layout` component. If a new page omits `p-6`, the layout will be inconsistent. Consider moving container padding to the `Layout` component wrapping `<Outlet />`.
|
||||
|
||||
### 1.2 Use of Design System Components vs Custom HTML
|
||||
|
||||
| Component | DashboardPage | LicensePage | AdminTenantsPage |
|
||||
|-----------|:---:|:---:|:---:|
|
||||
| Badge | Yes | Yes | Yes |
|
||||
| Button | Yes | - | - |
|
||||
| Card | Yes | Yes | Yes |
|
||||
| DataTable | - | - | Yes |
|
||||
| EmptyState | Yes | Yes | - |
|
||||
| KpiStrip | Yes | - | - |
|
||||
| Spinner | Yes | Yes | Yes |
|
||||
|
||||
**Issues found:**
|
||||
|
||||
- **LicensePage.tsx:166-170** — Raw `<button>` for "Show token" / "Hide token" toggle instead of DS `Button variant="ghost"`:
|
||||
```tsx
|
||||
<button
|
||||
type="button"
|
||||
className="text-sm text-primary-400 hover:text-primary-300 underline underline-offset-2 focus:outline-none"
|
||||
onClick={() => setTokenExpanded((v) => !v)}
|
||||
>
|
||||
```
|
||||
This uses hardcoded Tailwind color classes (`text-primary-400`, `hover:text-primary-300`) instead of design tokens or a DS Button.
|
||||
|
||||
- **LicensePage.tsx:174** — Raw `<div>` + `<code>` for token display instead of DS `CodeBlock` (which is available and supports `copyable`):
|
||||
```tsx
|
||||
<div className="mt-2 rounded bg-white/5 border border-white/10 p-3 overflow-x-auto">
|
||||
<code className="text-xs font-mono text-white/80 break-all">
|
||||
{license.token}
|
||||
</code>
|
||||
</div>
|
||||
```
|
||||
|
||||
- **AdminTenantsPage.tsx** — No empty state when `tenants` is empty. The DataTable renders with zero rows but no guidance for the admin.
|
||||
|
||||
### 1.3 Card/Section Grouping
|
||||
|
||||
- **DashboardPage** uses: KpiStrip + "Tenant Information" Card + "Server Management" Card. Good grouping.
|
||||
- **LicensePage** uses: "Validity" Card + "Features" Card + "Limits" Card + "License Token" Card. Well-structured.
|
||||
- **AdminTenantsPage** uses: single Card wrapping DataTable. Appropriate for a list view.
|
||||
|
||||
### 1.4 Typography
|
||||
|
||||
All pages use the same heading pattern:
|
||||
```tsx
|
||||
<h1 className="text-2xl font-semibold text-white">...</h1>
|
||||
```
|
||||
|
||||
**Issue:** `text-white` is hardcoded rather than using a DS color token like `var(--text-primary)`. This will break if the design system ever supports a light theme (the DS has `ThemeProvider` and a theme toggle in the TopBar). The same pattern appears:
|
||||
- `DashboardPage.tsx:73` — `text-white`
|
||||
- `LicensePage.tsx:85` — `text-white`
|
||||
- `AdminTenantsPage.tsx:62` — `text-white`
|
||||
|
||||
Similarly, muted text uses `text-white/60` and `text-white/80` throughout:
|
||||
- `DashboardPage.tsx:96` — `text-white/80`
|
||||
- `LicensePage.tsx:96,106,109` — `text-white/60`, `text-white`
|
||||
- `LicensePage.tsx:129` — `text-sm text-white`
|
||||
- `LicensePage.tsx:150` — `text-sm text-white/60`
|
||||
|
||||
These should use `var(--text-primary)` / `var(--text-secondary)` / `var(--text-muted)` from the design system.
|
||||
|
||||
### 1.5 Color Token Usage
|
||||
|
||||
**Positive:** The sign-in page CSS module (`SignInPage.module.css`) correctly uses DS variables:
|
||||
```css
|
||||
color: var(--text-primary); /* line 30 */
|
||||
color: var(--text-muted); /* line 40 */
|
||||
background: var(--bg-base); /* line 7 */
|
||||
font-family: var(--font-body); /* line 20 */
|
||||
```
|
||||
|
||||
**Negative:** The platform SPA pages bypass the design system's CSS variables entirely, using Tailwind utility classes with hardcoded dark-theme colors (`text-white`, `text-white/60`, `bg-white/5`, `border-white/10`, `divide-white/10`).
|
||||
|
||||
---
|
||||
|
||||
## 2. Interaction Patterns
|
||||
|
||||
### 2.1 Button Placement and Order
|
||||
|
||||
- **DashboardPage.tsx:81-87** — "Open Server Dashboard" button is top-right (standard). Also repeated inside a Card at line 119-125. Two identical CTAs on the same page is redundant.
|
||||
- No forms exist in the platform pages. No create/edit/delete operations are exposed in the UI (read-only dashboard).
|
||||
|
||||
### 2.2 Confirmation Dialogs for Destructive Actions
|
||||
|
||||
- The DS provides `ConfirmDialog` and `AlertDialog` — neither is used anywhere.
|
||||
- **AdminTenantsPage.tsx:47-57** — Row click silently switches tenant context and navigates to `/`. No confirmation dialog for context switching, which could be disorienting. The user clicks a row in the admin table, and their entire session context changes.
|
||||
|
||||
### 2.3 Loading States
|
||||
|
||||
All pages use the same loading pattern — centered `<Spinner />` in a fixed-height container:
|
||||
```tsx
|
||||
<div className="flex items-center justify-center h-64">
|
||||
<Spinner />
|
||||
</div>
|
||||
```
|
||||
|
||||
**Issues:**
|
||||
- Full-page auth loading screens (LoginPage, CallbackPage, ProtectedRoute, OrgResolver) use inline styles instead of Tailwind:
|
||||
```tsx
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', minHeight: '100vh' }}>
|
||||
```
|
||||
This is inconsistent with the page components which use Tailwind classes.
|
||||
|
||||
- The `main.tsx` app bootstrap loading (line 59) also uses inline styles. Six files use this identical inline style pattern — it should be a shared component or consistent class.
|
||||
|
||||
- No `Skeleton` components are used anywhere, despite the DS providing `Skeleton`. For the dashboard and license pages which fetch data, skeletons would give better perceived performance than a generic spinner.
|
||||
|
||||
### 2.4 Error Handling
|
||||
|
||||
- **API client (`api/client.ts`):** Errors are thrown as generic `Error` objects. No toast notifications on failure.
|
||||
- **LicensePage.tsx:63-69** — Shows `EmptyState` for `isError`. Good.
|
||||
- **DashboardPage.tsx** — No error state handling at all. If `useTenant()` or `useLicense()` fails, the page renders with fallback `-` values silently. No `isError` check.
|
||||
- **AdminTenantsPage.tsx** — No error state. If `useAllTenants()` fails, falls through to rendering the table with empty data.
|
||||
- **OrgResolver.tsx:88-89** — On error, renders `null` (blank screen). The user sees nothing — no error message, no retry option, no redirect. This is the worst error UX in the app.
|
||||
- No component imports or uses `useToast()` from the DS. Toasts are never shown for any operation.
|
||||
|
||||
### 2.5 Empty States
|
||||
|
||||
- **DashboardPage.tsx:57-63** — `EmptyState` for no tenant. Good.
|
||||
- **LicensePage.tsx:54-60** — `EmptyState` for no tenant. Good.
|
||||
- **LicensePage.tsx:63-69** — `EmptyState` for license fetch error. Good.
|
||||
- **AdminTenantsPage.tsx** — **Missing.** No empty state when `tenants` array is empty. DataTable will render an empty table body.
|
||||
|
||||
---
|
||||
|
||||
## 3. Component Usage
|
||||
|
||||
### 3.1 DS Imports by File
|
||||
|
||||
| File | DS Components Imported |
|
||||
|------|----------------------|
|
||||
| `main.tsx` | ThemeProvider, ToastProvider, BreadcrumbProvider, GlobalFilterProvider, CommandPaletteProvider, Spinner |
|
||||
| `Layout.tsx` | AppShell, Sidebar, TopBar |
|
||||
| `DashboardPage.tsx` | Badge, Button, Card, EmptyState, KpiStrip, Spinner |
|
||||
| `LicensePage.tsx` | Badge, Card, EmptyState, Spinner |
|
||||
| `AdminTenantsPage.tsx` | Badge, Card, DataTable, Spinner + Column type |
|
||||
| `LoginPage.tsx` | Spinner |
|
||||
| `CallbackPage.tsx` | Spinner |
|
||||
| `ProtectedRoute.tsx` | Spinner |
|
||||
| `OrgResolver.tsx` | Spinner |
|
||||
| `SignInPage.tsx` (sign-in) | Card, Input, Button, Alert, FormField |
|
||||
|
||||
### 3.2 Available but Unused DS Components
|
||||
|
||||
These DS components are relevant to the platform UI but unused:
|
||||
|
||||
| Component | Could be used for |
|
||||
|-----------|------------------|
|
||||
| `AlertDialog` / `ConfirmDialog` | Confirming tenant context switch in AdminTenantsPage |
|
||||
| `CodeBlock` | License token display (currently raw HTML) |
|
||||
| `Skeleton` | Loading states instead of spinner |
|
||||
| `Tooltip` | Badge hover explanations, info about features |
|
||||
| `StatusDot` | Tenant status indicators |
|
||||
| `Breadcrumb` / `useBreadcrumb` | Page navigation context (currently empty `[]`) |
|
||||
| `LoginForm` | Could replace the custom sign-in form (DS already has one) |
|
||||
| `useToast` | Error/success notifications |
|
||||
|
||||
### 3.3 Raw HTML Where DS Components Exist
|
||||
|
||||
1. **LicensePage.tsx:166-170** — Raw `<button>` instead of `Button variant="ghost"`
|
||||
2. **LicensePage.tsx:174-178** — Raw `<div><code>` instead of `CodeBlock`
|
||||
3. **Layout.tsx:26-62** — Four inline SVG icon components instead of using `lucide-react` icons (the DS depends on lucide-react)
|
||||
4. **DashboardPage.tsx:95-112** — Manual label/value list with `<div className="flex justify-between">` instead of using a DS pattern (the DS has no explicit key-value list component, so this is acceptable)
|
||||
|
||||
### 3.4 Styling Approach
|
||||
|
||||
- **Platform SPA pages:** Tailwind CSS utility classes (via class names like `space-y-6`, `p-6`, `flex`, `items-center`, etc.)
|
||||
- **Sign-in page:** CSS modules (`SignInPage.module.css`) with DS CSS variables
|
||||
- **Auth loading screens:** Inline `style={{}}` objects
|
||||
- **No CSS modules** in the platform SPA at all (zero `.module.css` files in `ui/src/`)
|
||||
|
||||
This is a three-way inconsistency: Tailwind in pages, CSS modules in sign-in, inline styles in auth components.
|
||||
|
||||
---
|
||||
|
||||
## 4. Navigation
|
||||
|
||||
### 4.1 Sidebar
|
||||
|
||||
**File:** `ui/src/components/Layout.tsx:70-118`
|
||||
|
||||
The sidebar uses `Sidebar.Section` with `open={false}` and `{null}` children as a workaround to make sections act as navigation links (via `onToggle`). This is a semantic misuse — sections are designed as collapsible containers, not nav links.
|
||||
|
||||
```tsx
|
||||
<Sidebar.Section
|
||||
icon={<DashboardIcon />}
|
||||
label="Dashboard"
|
||||
open={false}
|
||||
onToggle={() => navigate('/')}
|
||||
>
|
||||
{null}
|
||||
</Sidebar.Section>
|
||||
```
|
||||
|
||||
**Issues:**
|
||||
- No `active` state is set on any section. The DS supports `active?: boolean` on `SidebarSectionProps` (line 988 of DS types), but it's never passed. The user has no visual indicator of which page they're on.
|
||||
- `collapsed={false}` is hardcoded with `onCollapseToggle={() => {}}` — the sidebar cannot be collapsed. This is a no-op handler.
|
||||
- Only three nav items: Dashboard, License, Platform (admin-only). Very sparse.
|
||||
|
||||
### 4.2 "Open Server Dashboard"
|
||||
|
||||
Two implementations, both identical:
|
||||
1. **Sidebar footer** (`Layout.tsx:112-116`): `Sidebar.FooterLink` with `window.open('/server/', '_blank', 'noopener')`
|
||||
2. **Dashboard page** (`DashboardPage.tsx:84`): Primary Button, same `window.open` call
|
||||
3. **Dashboard page** (`DashboardPage.tsx:120-125`): Secondary Button in a Card, same `window.open` call
|
||||
|
||||
Three separate "Open Server Dashboard" triggers on the dashboard. The footer link is good; the two dashboard buttons are redundant.
|
||||
|
||||
### 4.3 Breadcrumbs
|
||||
|
||||
**File:** `Layout.tsx:124` — `<TopBar breadcrumb={[]} ... />`
|
||||
|
||||
Breadcrumbs are permanently empty. The DS provides `useBreadcrumb()` hook (exported, see line 1255 of DS types) that pages can call to set page-specific breadcrumbs, but none of the pages use it. The TopBar renders an empty breadcrumb area.
|
||||
|
||||
### 4.4 User Menu / Avatar
|
||||
|
||||
**File:** `Layout.tsx:125-126`
|
||||
|
||||
```tsx
|
||||
<TopBar
|
||||
user={username ? { name: username } : undefined}
|
||||
onLogout={logout}
|
||||
/>
|
||||
```
|
||||
|
||||
The TopBar's `user` prop triggers a `Dropdown` with only a "Logout" option. The avatar is rendered by the DS using the `Avatar` component with the user's name.
|
||||
|
||||
**Issue:** When `username` is `null` (common if the Logto ID token doesn't have `username`, `name`, or `email` claims), no user indicator is shown at all — no avatar, no logout button. The user has no way to log out from the UI.
|
||||
|
||||
---
|
||||
|
||||
## 5. Header Bar
|
||||
|
||||
### 5.1 Shared TopBar with Server
|
||||
|
||||
The platform SPA and the server SPA both use the same `TopBar` component from `@cameleer/design-system`. This means they share identical header chrome.
|
||||
|
||||
### 5.2 Irrelevant Controls on Platform Pages
|
||||
|
||||
**Critical issue.** The `TopBar` component (DS source, lines 5569-5588 of `index.es.js`) **always** renders:
|
||||
|
||||
1. **Status filter pills** (Completed, Warning, Error, Running) — `ButtonGroup` with global filter status values
|
||||
2. **Time range dropdown** — `TimeRangeDropdown` with presets like "Last 1h", "Last 24h"
|
||||
3. **Auto-refresh toggle** — "AUTO" / "MANUAL" button
|
||||
4. **Theme toggle** — Light/dark mode switch
|
||||
5. **Command palette search** — "Search... Ctrl+K" button
|
||||
|
||||
These controls are hardcoded in the DS `TopBar` component. They read from `useGlobalFilters()` and operate on exchange status filters and time ranges — concepts that are **completely irrelevant** to the SaaS platform pages (Dashboard, License, Admin Tenants).
|
||||
|
||||
The platform wraps everything in `GlobalFilterProvider` (in `main.tsx:96`), which initializes the filter state, but nothing in the platform UI reads or uses these filters. They are dead UI elements that confuse users.
|
||||
|
||||
**Recommendation:** Either:
|
||||
- The DS should make these controls optional/configurable on `TopBar`
|
||||
- The platform should use a simpler header component
|
||||
- The platform should not wrap in `GlobalFilterProvider` / `CommandPaletteProvider` (but this may cause runtime errors if TopBar assumes they exist)
|
||||
|
||||
---
|
||||
|
||||
## 6. Specific Issues
|
||||
|
||||
### 6.1 Label/Value Formatting — "Slugdefault" Concatenation Bug
|
||||
|
||||
**Not found in source code.** The source code properly formats label/value pairs with `flex justify-between` layout:
|
||||
|
||||
```tsx
|
||||
// DashboardPage.tsx:96-99
|
||||
<div className="flex justify-between text-white/80">
|
||||
<span>Slug</span>
|
||||
<span className="font-mono">{tenant?.slug ?? '-'}</span>
|
||||
</div>
|
||||
```
|
||||
|
||||
If "Slugdefault" concatenation is visible in the UI, it's a **rendering/CSS issue** rather than a template bug — the `flex justify-between` may collapse if the container is too narrow, or there may be a DS Card padding issue causing the spans to not separate. The code itself has proper separation.
|
||||
|
||||
Similarly for limits on the License page:
|
||||
```tsx
|
||||
// LicensePage.tsx:147-155
|
||||
<span className="text-sm text-white/60">{label}</span>
|
||||
<span className="text-sm font-mono text-white">{value !== undefined ? value : '—'}</span>
|
||||
```
|
||||
|
||||
Labels and values are in separate `<span>` elements within `flex justify-between` containers. The code is correct.
|
||||
|
||||
### 6.2 Badge Colors
|
||||
|
||||
**Feature badges (LicensePage.tsx:130-133):**
|
||||
```tsx
|
||||
<Badge
|
||||
label={enabled ? 'Enabled' : 'Not included'}
|
||||
color={enabled ? 'success' : 'auto'}
|
||||
/>
|
||||
```
|
||||
|
||||
- Enabled features: `color="success"` (green) — appropriate
|
||||
- Disabled features: `color="auto"` — this uses the DS's auto-color logic (hash-based). For a disabled/not-included state, `color="error"` or a neutral muted variant would be more appropriate to clearly communicate "not available."
|
||||
|
||||
**Tenant status badges (DashboardPage.tsx:102-105, AdminTenantsPage.tsx:24-29):**
|
||||
```tsx
|
||||
color={tenant?.status === 'ACTIVE' ? 'success' : 'warning'}
|
||||
color={row.status === 'ACTIVE' ? 'success' : 'warning'}
|
||||
```
|
||||
|
||||
- ACTIVE: green — appropriate
|
||||
- Anything else (SUSPENDED, PENDING): yellow/warning — reasonable but SUSPENDED should arguably be `error` (red)
|
||||
|
||||
**Tier badges:** Use `tierColor()` function but it's defined differently in each file:
|
||||
|
||||
- `DashboardPage.tsx:12-18` maps: enterprise->success, pro->primary, starter->warning
|
||||
- `LicensePage.tsx:25-33` maps: BUSINESS->success, HIGH->primary, MID->warning, LOW->error
|
||||
|
||||
These use **different tier names** (enterprise/pro/starter vs BUSINESS/HIGH/MID/LOW). One is for tenant tiers, the other for license tiers, but the inconsistency suggests either the data model has diverged or one mapping is stale.
|
||||
|
||||
### 6.3 Sign-In Page (`ui/sign-in/src/`)
|
||||
|
||||
**Positive findings:**
|
||||
- Uses DS components: `Card`, `Input`, `Button`, `Alert`, `FormField`
|
||||
- Uses CSS modules with DS CSS variables (`var(--bg-base)`, `var(--text-primary)`, etc.)
|
||||
- Proper form with `aria-label="Sign in"`, `autoComplete` attributes
|
||||
- Loading state on submit button via `loading` prop
|
||||
- Error display via DS `Alert variant="error"`
|
||||
- Creative rotating subtitle strings — good personality touch
|
||||
|
||||
**Issues:**
|
||||
1. **No `ThemeProvider` wrapper** (`sign-in/src/main.tsx`):
|
||||
```tsx
|
||||
createRoot(document.getElementById('root')!).render(
|
||||
<StrictMode>
|
||||
<App />
|
||||
</StrictMode>,
|
||||
);
|
||||
```
|
||||
The sign-in page imports `@cameleer/design-system/style.css` which provides CSS variable defaults, so it works. But the theme toggle won't function, and if the DS ever requires `ThemeProvider` for initialization, this will break.
|
||||
|
||||
2. **No `ToastProvider`** — if any DS component internally uses `useToast()`, it will throw.
|
||||
|
||||
3. **Hardcoded branding** (`SignInPage.tsx:61`):
|
||||
```tsx
|
||||
cameleer3
|
||||
```
|
||||
The brand name is hardcoded text, not sourced from configuration.
|
||||
|
||||
4. **`React` import unused** (`SignInPage.tsx:1`): `useMemo` and `useState` are imported from `react` but the `import React` default import is absent, which is fine for React 19.
|
||||
|
||||
5. **No "forgot password" flow** — the form has username + password only. No recovery link. The DS `LoginForm` component supports `onForgotPassword` and `onSignUp` callbacks.
|
||||
|
||||
---
|
||||
|
||||
## 7. Architecture Observations
|
||||
|
||||
### 7.1 Provider Stack Over-provisioning
|
||||
|
||||
`main.tsx` wraps the app in:
|
||||
```
|
||||
ThemeProvider > ToastProvider > BreadcrumbProvider > GlobalFilterProvider > CommandPaletteProvider
|
||||
```
|
||||
|
||||
`GlobalFilterProvider` and `CommandPaletteProvider` are server-dashboard concepts (exchange status filters, time range, search). They are unused by any platform page but are required because `TopBar` reads from them internally. This creates coupling between the server's observability UI concerns and the SaaS platform pages.
|
||||
|
||||
### 7.2 Route Guard Nesting
|
||||
|
||||
The route structure is:
|
||||
```
|
||||
ProtectedRoute > OrgResolver > Layout > (pages)
|
||||
```
|
||||
|
||||
`OrgResolver` fetches `/api/me` and resolves tenant context. If it fails (`isError`), it renders `null` — a blank screen inside the Layout shell. This means the sidebar and TopBar render but the content area is completely empty with no explanation.
|
||||
|
||||
### 7.3 Unused Import
|
||||
|
||||
- `LicensePage.tsx:1` imports `React` and `useState` — `React` import is not needed with React 19's JSX transform, and `useState` is used so that's fine. But `React` as a namespace import isn't used.
|
||||
|
||||
### 7.4 DataTable Requires `id` Field
|
||||
|
||||
`AdminTenantsPage.tsx:67` passes `tenants` to `DataTable`. The DS type requires `T extends { id: string }`. The `TenantResponse` type has `id: string`, so this works, but the `createdAt` column (line 31) renders the raw ISO timestamp string without formatting — unlike DashboardPage which formats it with `toLocaleDateString()`.
|
||||
|
||||
---
|
||||
|
||||
## 8. Summary of Issues by Severity
|
||||
|
||||
### High Priority
|
||||
| # | Issue | File(s) | Line(s) |
|
||||
|---|-------|---------|---------|
|
||||
| H1 | TopBar shows irrelevant status filters, time range, auto-refresh for platform pages | `Layout.tsx` / DS `TopBar` | 122-128 |
|
||||
| H2 | OrgResolver error state renders blank screen (no error UI) | `OrgResolver.tsx` | 88-89 |
|
||||
| H3 | Hardcoded `text-white` colors break light theme | All pages | Multiple |
|
||||
|
||||
### Medium Priority
|
||||
| # | Issue | File(s) | Line(s) |
|
||||
|---|-------|---------|---------|
|
||||
| M1 | No active state on sidebar navigation items | `Layout.tsx` | 79-108 |
|
||||
| M2 | Breadcrumbs permanently empty | `Layout.tsx` | 124 |
|
||||
| M3 | DashboardPage has no error handling for failed API calls | `DashboardPage.tsx` | 23-26 |
|
||||
| M4 | AdminTenantsPage missing empty state | `AdminTenantsPage.tsx` | 67-72 |
|
||||
| M5 | AdminTenantsPage row click silently switches tenant context | `AdminTenantsPage.tsx` | 47-57 |
|
||||
| M6 | Toasts never used despite ToastProvider being mounted | All pages | - |
|
||||
| M7 | Raw `<button>` and `<code>` instead of DS components in LicensePage | `LicensePage.tsx` | 166-178 |
|
||||
| M8 | AdminTenantsPage `createdAt` column renders raw ISO string | `AdminTenantsPage.tsx` | 31 |
|
||||
| M9 | `tierColor()` defined twice with different tier mappings | `DashboardPage.tsx`, `LicensePage.tsx` | 12-18, 25-33 |
|
||||
| M10 | "Not included" feature badge uses `color="auto"` instead of muted/neutral | `LicensePage.tsx` | 133 |
|
||||
|
||||
### Low Priority
|
||||
| # | Issue | File(s) | Line(s) |
|
||||
|---|-------|---------|---------|
|
||||
| L1 | Three "Open Server Dashboard" buttons/links on dashboard | `Layout.tsx`, `DashboardPage.tsx` | 112-116, 81-87, 119-125 |
|
||||
| L2 | Inconsistent loading style (inline styles vs Tailwind) | Auth files vs pages | Multiple |
|
||||
| L3 | No Skeleton loading used (all Spinner) | All pages | - |
|
||||
| L4 | Sidebar collapse disabled (no-op handler) | `Layout.tsx` | 71 |
|
||||
| L5 | Sign-in page missing ThemeProvider wrapper | `sign-in/src/main.tsx` | 6-9 |
|
||||
| L6 | Sign-in page has no forgot-password or sign-up link | `sign-in/src/SignInPage.tsx` | - |
|
||||
| L7 | Custom SVG icons in Layout instead of lucide-react | `Layout.tsx` | 26-62 |
|
||||
| L8 | Username null = no logout button visible | `Layout.tsx` | 125-126 |
|
||||
| L9 | Page padding `p-6` repeated per-page instead of in Layout | All pages | - |
|
||||
413
docs/superpowers/specs/2026-04-09-saas-ux-polish-design.md
Normal file
413
docs/superpowers/specs/2026-04-09-saas-ux-polish-design.md
Normal file
@@ -0,0 +1,413 @@
|
||||
# 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 audit
|
||||
- `source-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 section
|
||||
- `LicensePage.tsx:94-115` — Validity section
|
||||
- `LicensePage.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:
|
||||
|
||||
```tsx
|
||||
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:
|
||||
|
||||
```tsx
|
||||
// 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:
|
||||
1. Sidebar footer link (always accessible)
|
||||
2. 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:
|
||||
|
||||
```tsx
|
||||
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:
|
||||
|
||||
```tsx
|
||||
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:
|
||||
|
||||
```tsx
|
||||
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):
|
||||
```tsx
|
||||
// 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:
|
||||
|
||||
```tsx
|
||||
<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":
|
||||
|
||||
```tsx
|
||||
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:
|
||||
|
||||
```tsx
|
||||
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:
|
||||
|
||||
```tsx
|
||||
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-18` maps enterprise/pro/starter
|
||||
- `LicensePage.tsx:25-33` maps BUSINESS/HIGH/MID/LOW
|
||||
|
||||
**Fix:** Extract a single `tierColor()` to a shared utility (`ui/src/utils/tier.ts`). Map all known tier names:
|
||||
|
||||
```typescript
|
||||
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 "cameleer3" — internal repo name, not product brand.
|
||||
|
||||
**Fix:** Change to "Cameleer" (product name). Update the page title from "Sign in — cameleer3" 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)
|
||||
Reference in New Issue
Block a user