diff --git a/frontend/src/components/layout/Header.test.tsx b/frontend/src/components/layout/Header.test.tsx index 297b8a6..29d5531 100644 --- a/frontend/src/components/layout/Header.test.tsx +++ b/frontend/src/components/layout/Header.test.tsx @@ -1,13 +1,30 @@ -import { describe, it, expect, beforeEach } from 'vitest'; +import { describe, it, expect, beforeEach, vi } from 'vitest'; import { render, screen, fireEvent } from '@/test/utils'; import { Header } from './Header'; import { useThemeStore } from '@/stores/themeStore'; +import { api } from '@/services/api'; + +vi.mock('@/services/api', () => ({ + api: { + get: vi.fn().mockResolvedValue({ success: false, error: 'not mocked', data: undefined }), + }, +})); + +const mockApi = vi.mocked(api); + +function mockSession(session: object) { + mockApi.get.mockResolvedValue({ + success: true, + data: { session }, + }); +} describe('Header', () => { beforeEach(() => { // Reset theme store useThemeStore.setState({ isDark: false }); document.documentElement.classList.remove('dark'); + mockApi.get.mockResolvedValue({ success: false, error: 'not mocked', data: undefined }); }); it('should render the app title', () => { @@ -61,4 +78,36 @@ describe('Header', () => { expect(button).toBeInTheDocument(); }); }); + + describe('SteamGifts session indicator', () => { + it('shows connected icon with username tooltip when session is valid', async () => { + mockSession({ configured: true, valid: true, username: 'TestUser', error: null }); + render(
); + + const icon = await screen.findByLabelText('Connected to SteamGifts as TestUser'); + expect(icon).toBeInTheDocument(); + }); + + it('shows invalid-session icon when session expired', async () => { + mockSession({ configured: true, valid: false, username: null, error: 'expired' }); + render(
); + + const icon = await screen.findByLabelText(/session invalid or expired/i); + expect(icon).toBeInTheDocument(); + }); + + it('shows not-configured icon when no session is set', async () => { + mockSession({ configured: false, valid: false, username: null, error: null }); + render(
); + + const icon = await screen.findByLabelText(/session not configured/i); + expect(icon).toBeInTheDocument(); + }); + + it('renders no indicator while session status is unknown', () => { + render(
); + + expect(screen.queryByLabelText(/steamgifts/i)).not.toBeInTheDocument(); + }); + }); }); diff --git a/frontend/src/components/layout/Header.tsx b/frontend/src/components/layout/Header.tsx index ca1d1c9..150ac4b 100644 --- a/frontend/src/components/layout/Header.tsx +++ b/frontend/src/components/layout/Header.tsx @@ -1,6 +1,6 @@ -import { Sun, Moon, Activity, Wifi, WifiOff } from 'lucide-react'; +import { Sun, Moon, Activity, Wifi, WifiOff, UserCheck, UserX } from 'lucide-react'; import { useThemeStore } from '@/stores/themeStore'; -import { useWebSocketStatus } from '@/hooks'; +import { useSessionStatus, useWebSocketStatus } from '@/hooks'; interface HeaderProps { schedulerRunning?: boolean; @@ -13,6 +13,16 @@ interface HeaderProps { export function Header({ schedulerRunning = false, schedulerPaused = false }: HeaderProps) { const { isDark, toggle } = useThemeStore(); const { isConnected } = useWebSocketStatus(); + const { data: session } = useSessionStatus(); + + let sessionTitle = 'SteamGifts session not configured'; + if (session?.configured && session.valid) { + sessionTitle = session.username + ? `Connected to SteamGifts as ${session.username}` + : 'Connected to SteamGifts'; + } else if (session?.configured) { + sessionTitle = 'SteamGifts session invalid or expired — update it in Settings'; + } // Determine status color and text let statusColor = 'text-gray-400'; @@ -37,6 +47,21 @@ export function Header({ schedulerRunning = false, schedulerPaused = false }: He
+ {/* SteamGifts Session Indicator */} + {session && ( +
+ {session.configured && session.valid ? ( + + ) : ( + + )} +
+ )} + {/* WebSocket Connection Indicator */}
{ + const response = await api.get('/api/v1/analytics/dashboard'); + if (!response.success) { + throw new Error(response.error || 'Failed to fetch dashboard data'); + } + return response.data; +} + /** * Fetch dashboard overview data */ export function useDashboard() { return useQuery({ queryKey: analyticsKeys.dashboard, - queryFn: async () => { - const response = await api.get('/api/v1/analytics/dashboard'); - if (!response.success) { - throw new Error(response.error || 'Failed to fetch dashboard data'); - } - return response.data; - }, + queryFn: fetchDashboard, // Dashboard refreshes every 30 seconds refetchInterval: 30_000, }); } +/** + * SteamGifts session status for the header indicator. + * + * Shares the dashboard query cache but deliberately does NOT poll: the + * dashboard endpoint live-tests the session against SteamGifts, and the + * header is mounted on every page. On the Dashboard page the shared cache + * is refreshed by useDashboard's interval anyway. + */ +export function useSessionStatus() { + return useQuery({ + queryKey: analyticsKeys.dashboard, + queryFn: fetchDashboard, + staleTime: 60_000, + select: (data) => data?.session, + }); +} + /** Backend response for entries/summary */ interface EntrySummaryResponse { total_entries: number; diff --git a/frontend/src/pages/Dashboard.tsx b/frontend/src/pages/Dashboard.tsx index ac9ac3b..787b92f 100644 --- a/frontend/src/pages/Dashboard.tsx +++ b/frontend/src/pages/Dashboard.tsx @@ -1,5 +1,5 @@ import { useState, useEffect } from 'react'; -import { Play, Pause, Square, RefreshCw, Zap, Gift, Clock, ExternalLink, X, Trophy, RotateCw, AlertTriangle, Settings, CheckCircle, Shield, ShieldAlert, ShieldQuestion } from 'lucide-react'; +import { Play, Pause, Square, RefreshCw, Zap, Gift, Clock, ExternalLink, X, Trophy, RotateCw, AlertTriangle, Settings, Shield, ShieldAlert, ShieldQuestion } from 'lucide-react'; import { SiSteam } from 'react-icons/si'; import { Card, Button, Badge, Loading, CardSkeleton } from '@/components/common'; import { useDashboard, useSchedulerStatus, useSchedulerControl, useGiveaways, useRemoveEntry } from '@/hooks'; @@ -648,18 +648,6 @@ function SessionStatusBanner({ session }: SessionStatusBannerProps) { ); } - // Session valid - show connected status (compact) - return ( -
-
- - - Connected to SteamGifts - {session.username && ( - as {session.username} - )} - -
-
- ); + // Session valid: no banner — the header shows the connected indicator. + return null; }