From f77797881b01f794ddf8a4397af0b4d5d482e2b5 Mon Sep 17 00:00:00 2001 From: Saravana Kumar Date: Sat, 30 May 2026 21:02:21 +0000 Subject: [PATCH] feat(admin-web): add dashboard retry state --- .../e2e/dashboard-reliability.spec.ts | 105 ++++++++++++++++++ dashboards/admin-web/playwright.config.ts | 13 ++- .../admin-web/src/app/(dashboard)/page.tsx | 47 ++++++++ 3 files changed, 160 insertions(+), 5 deletions(-) create mode 100644 dashboards/admin-web/e2e/dashboard-reliability.spec.ts diff --git a/dashboards/admin-web/e2e/dashboard-reliability.spec.ts b/dashboards/admin-web/e2e/dashboard-reliability.spec.ts new file mode 100644 index 00000000..c5d2c6f8 --- /dev/null +++ b/dashboards/admin-web/e2e/dashboard-reliability.spec.ts @@ -0,0 +1,105 @@ +import { test, expect, type Page, type Route } from '@playwright/test'; + +function authenticate(page: Page) { + return page.addInitScript(() => { + localStorage.setItem('admin_access_token', 'mock-token'); + localStorage.setItem('admin_refresh_token', 'mock-refresh'); + localStorage.setItem( + 'admin_auth_user', + JSON.stringify({ + email: 'admin@example.com', + name: 'Admin User', + role: 'super_admin', + }) + ); + }); +} + +async function fulfillJson(route: Route, body: unknown, status = 200) { + await route.fulfill({ + status, + contentType: 'application/json', + body: JSON.stringify(body), + }); +} + +test.describe('Admin dashboard reliability', () => { + test('shows an in-page retry path when dashboard APIs fail', async ({ page }) => { + await authenticate(page); + + let recover = false; + await page.route('**/api/dashboard/stats', route => + recover + ? fulfillJson(route, { + users: { total: 42, byPlan: { pro: 20 } }, + tokens: { active: 7 }, + usage: { totalWords: 8400, totalDictations: 120, totalCost: 12.34 }, + audit: { total: 2, failedLogins: 0 }, + }) + : fulfillJson(route, { error: 'stats unavailable' }, 503) + ); + await page.route('**/api/usage**', route => + recover + ? fulfillJson(route, { + records: [ + { + date: '2026-05-30', + tokensUsed: 8400, + dictations: 120, + costUsd: 12.34, + model: 'gpt-4o-mini', + }, + ], + }) + : fulfillJson(route, { error: 'usage unavailable' }, 503) + ); + await page.route('**/api/users**', route => + recover + ? fulfillJson(route, { + users: [ + { + id: 'u1', + name: 'Admin User', + email: 'admin@example.com', + plan: 'pro', + status: 'active', + createdAt: '2026-05-01T00:00:00Z', + lastActive: '2026-05-30T00:00:00Z', + totalTokensUsed: 8400, + totalRequests: 120, + monthlySpend: 12.34, + }, + ], + total: 1, + byPlan: { pro: 1 }, + }) + : fulfillJson(route, { error: 'users unavailable' }, 503) + ); + await page.route('**/api/analytics/revenue**', route => + recover + ? fulfillJson(route, { + mrr: 199, + arr: 2388, + mrrChange: 8, + totalRevenue: 2388, + revenueByMonth: [], + churnRate: 2, + churnCount: 1, + ltv: 1200, + arpu: 99, + newSubscriptions: 3, + canceledSubscriptions: 1, + }) + : fulfillJson(route, { error: 'revenue unavailable' }, 503) + ); + + await page.goto('/'); + await expect(page.getByRole('heading', { name: /could not load dashboard/i })).toBeVisible(); + + recover = true; + await page.getByRole('button', { name: /retry/i }).click(); + await expect(page.getByRole('heading', { name: 'Dashboard' })).toBeVisible(); + await expect(page.getByText('42', { exact: true }).first()).toBeVisible(); + await expect(page.getByRole('main').getByText('Admin User')).toBeVisible(); + }); +}); diff --git a/dashboards/admin-web/playwright.config.ts b/dashboards/admin-web/playwright.config.ts index 369fabd0..105f6f0e 100644 --- a/dashboards/admin-web/playwright.config.ts +++ b/dashboards/admin-web/playwright.config.ts @@ -1,5 +1,8 @@ import { defineConfig, devices } from '@playwright/test'; +const port = Number(process.env.ADMIN_WEB_E2E_PORT ?? 3101); +const baseURL = process.env.ADMIN_WEB_E2E_URL ?? `http://127.0.0.1:${port}`; + export default defineConfig({ testDir: './e2e', fullyParallel: true, @@ -8,7 +11,7 @@ export default defineConfig({ workers: process.env.CI ? 1 : undefined, reporter: 'html', use: { - baseURL: 'http://localhost:3001', + baseURL, trace: 'on-first-retry', }, projects: [ @@ -18,9 +21,9 @@ export default defineConfig({ }, ], webServer: { - command: 'npm run dev', - url: 'http://localhost:3001', - reuseExistingServer: !process.env.CI, - timeout: 30_000, + command: `pnpm exec next dev -H 127.0.0.1 -p ${port}`, + url: baseURL, + reuseExistingServer: process.env.ADMIN_WEB_REUSE_SERVER === '1', + timeout: 60_000, }, }); diff --git a/dashboards/admin-web/src/app/(dashboard)/page.tsx b/dashboards/admin-web/src/app/(dashboard)/page.tsx index 9211b795..7b516d90 100644 --- a/dashboards/admin-web/src/app/(dashboard)/page.tsx +++ b/dashboards/admin-web/src/app/(dashboard)/page.tsx @@ -12,6 +12,7 @@ import { ArrowDownRight, RefreshCw, Cpu, + AlertCircle, } from 'lucide-react'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Badge } from '@/components/ui/badge'; @@ -218,6 +219,7 @@ export default function DashboardPage() { const [revenue, setRevenue] = useState(null); const [loading, setLoading] = useState(true); const [refreshing, setRefreshing] = useState(false); + const [loadError, setLoadError] = useState(null); const [lastUpdated, setLastUpdated] = useState(null); const fetchData = useCallback(async (isRefresh = false) => { @@ -230,15 +232,29 @@ export default function DashboardPage() { apiGetRevenueAnalytics(6), ]); + let successfulSections = 0; + const failures: string[] = []; + if (statsRes.status === 'fulfilled' && statsRes.value.data) { + successfulSections += 1; setStats(prev => mergeApiStats(prev, statsRes.value.data!)); + } else { + failures.push('dashboard stats'); } if (usageRes.status === 'fulfilled' && usageRes.value.data?.records?.length) { + successfulSections += 1; const metrics = usageRecordsToDailyMetrics(usageRes.value.data.records); setDailyMetrics(metrics); setModelUsage(buildModelUsage(usageRes.value.data.records)); + } else if (usageRes.status === 'fulfilled' && usageRes.value.data) { + successfulSections += 1; + setDailyMetrics([]); + setModelUsage([]); + } else { + failures.push('usage'); } if (usersRes.status === 'fulfilled' && usersRes.value.data?.users?.length) { + successfulSections += 1; setRecentUsers( usersRes.value.data.users .sort((a, b) => b.lastActive.localeCompare(a.lastActive)) @@ -256,9 +272,22 @@ export default function DashboardPage() { monthlySpend: u.monthlySpend, })) ); + } else if (usersRes.status === 'fulfilled' && usersRes.value.data) { + successfulSections += 1; + setRecentUsers([]); + } else { + failures.push('users'); } if (revenueRes.status === 'fulfilled' && revenueRes.value.data) { + successfulSections += 1; setRevenue(revenueRes.value.data); + } else { + failures.push('revenue'); + } + if (successfulSections === 0) { + setLoadError(`Could not load ${failures.join(', ')}. Check the API connection and retry.`); + } else { + setLoadError(null); } setLastUpdated(new Date()); } finally { @@ -303,6 +332,24 @@ export default function DashboardPage() {

+ {loadError && !loading && ( + + +
+ +
+

Could not load dashboard

+

{loadError}

+
+
+ +
+
+ )} + {/* KPI Cards */} {loading ? (