diff --git a/playwright.config.ts b/playwright.config.ts index f50bcea..4a5c30d 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -18,9 +18,18 @@ export default defineConfig({ { name: 'chromium', use: { ...devices['Desktop Chrome'] } } ], webServer: { - command: 'pnpm start', + command: 'node .next/standalone/server.js', url: 'http://127.0.0.1:3005', reuseExistingServer: true, - timeout: 30_000, + timeout: 120_000, + env: { + ...process.env, + HOSTNAME: process.env.HOSTNAME || '127.0.0.1', + PORT: process.env.PORT || '3005', + MC_DISABLE_RATE_LIMIT: process.env.MC_DISABLE_RATE_LIMIT || '1', + API_KEY: process.env.API_KEY || 'test-api-key-e2e-12345', + AUTH_USER: process.env.AUTH_USER || 'testadmin', + AUTH_PASS: process.env.AUTH_PASS || 'testpass1234!', + }, } }) diff --git a/src/app/[[...panel]]/page.tsx b/src/app/[[...panel]]/page.tsx index 0b0dcaa..e7d05dd 100644 --- a/src/app/[[...panel]]/page.tsx +++ b/src/app/[[...panel]]/page.tsx @@ -1,7 +1,7 @@ 'use client' import { useEffect, useState } from 'react' -import { usePathname } from 'next/navigation' +import { usePathname, useRouter } from 'next/navigation' import { NavRail } from '@/components/layout/nav-rail' import { HeaderBar } from '@/components/layout/header-bar' import { LiveFeed } from '@/components/layout/live-feed' @@ -42,6 +42,7 @@ import { useServerEvents } from '@/lib/use-server-events' import { useMissionControl } from '@/store' export default function Home() { + const router = useRouter() const { connect } = useWebSocket() const { activeTab, setActiveTab, setCurrentUser, setDashboardMode, setGatewayAvailable, setSubscription, setUpdateAvailable, liveFeedOpen, toggleLiveFeed } = useMissionControl() @@ -62,7 +63,13 @@ export default function Home() { // Fetch current user fetch('/api/auth/me') - .then(res => res.ok ? res.json() : null) + .then(async (res) => { + if (res.ok) return res.json() + if (res.status === 401) { + router.replace(`/login?next=${encodeURIComponent(pathname)}`) + } + return null + }) .then(data => { if (data?.user) setCurrentUser(data.user) }) .catch(() => {}) @@ -120,7 +127,7 @@ export default function Home() { const wsUrl = explicitWsUrl || `${gatewayProto}://${gatewayHost}:${gatewayPort}` connect(wsUrl, wsToken) }) - }, [connect, setCurrentUser, setDashboardMode, setGatewayAvailable, setSubscription, setUpdateAvailable]) + }, [connect, pathname, router, setCurrentUser, setDashboardMode, setGatewayAvailable, setSubscription, setUpdateAvailable]) if (!isClient) { return ( diff --git a/src/components/panels/agent-squad-panel-phase3.tsx b/src/components/panels/agent-squad-panel-phase3.tsx index 97b827c..9dad847 100644 --- a/src/components/panels/agent-squad-panel-phase3.tsx +++ b/src/components/panels/agent-squad-panel-phase3.tsx @@ -96,7 +96,14 @@ export function AgentSquadPanelPhase3() { setSyncToast(null) try { const response = await fetch('/api/agents/sync', { method: 'POST' }) + if (response.status === 401) { + window.location.assign('/login?next=%2Fagents') + return + } const data = await response.json() + if (response.status === 403) { + throw new Error('Admin access required for agent sync') + } if (!response.ok) throw new Error(data.error || 'Sync failed') setSyncToast(`Synced ${data.synced} agents (${data.created} new, ${data.updated} updated)`) fetchAgents() @@ -116,7 +123,17 @@ export function AgentSquadPanelPhase3() { if (agents.length === 0) setLoading(true) const response = await fetch('/api/agents') - if (!response.ok) throw new Error('Failed to fetch agents') + if (response.status === 401) { + window.location.assign('/login?next=%2Fagents') + return + } + if (response.status === 403) { + throw new Error('Access denied') + } + if (!response.ok) { + const data = await response.json().catch(() => ({})) + throw new Error(data.error || 'Failed to fetch agents') + } const data = await response.json() setAgents(data.agents || []) diff --git a/src/components/panels/settings-panel.tsx b/src/components/panels/settings-panel.tsx index 3573d35..eacf25f 100644 --- a/src/components/panels/settings-panel.tsx +++ b/src/components/panels/settings-panel.tsx @@ -45,12 +45,17 @@ export function SettingsPanel() { const fetchSettings = useCallback(async () => { try { const res = await fetch('/api/settings') + if (res.status === 401) { + window.location.assign('/login?next=%2Fsettings') + return + } if (res.status === 403) { setError('Admin access required') return } if (!res.ok) { - setError('Failed to load settings') + const data = await res.json().catch(() => ({})) + setError(data.error || 'Failed to load settings') return } const data = await res.json() diff --git a/src/lib/websocket.ts b/src/lib/websocket.ts index df6c9e9..e670222 100644 --- a/src/lib/websocket.ts +++ b/src/lib/websocket.ts @@ -59,6 +59,8 @@ export function useWebSocket() { const pingCounterRef = useRef(0) const pingSentTimestamps = useRef>(new Map()) const missedPongsRef = useRef(0) + // Compat flag for gateway versions that may not implement ping RPC. + const gatewaySupportsPingRef = useRef(true) const { connection, @@ -116,6 +118,7 @@ export function useWebSocket() { pingIntervalRef.current = setInterval(() => { if (!wsRef.current || wsRef.current.readyState !== WebSocket.OPEN || !handshakeCompleteRef.current) return + if (!gatewaySupportsPingRef.current) return // Check missed pongs if (missedPongsRef.current >= MAX_MISSED_PONGS) {