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
{loadError}
+