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;
}