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 ? (