import React from 'react'; import './App.css'; import DeathwatchRoller from './components/DeathwatchRoller'; import RequisitionShop from './components/RequisitionShop'; import PlayerTab from './components/PlayerTab'; import RulesTab from './components/RulesTab'; import BestiaryTab from './components/BestiaryTab'; import WeaponsTab from './components/WeaponsTab'; import GMKit from './components/GMKit'; import PlayerManagement from './components/PlayerManagement'; import MissionTab from './components/MissionTab'; import MissionSimTab from './components/MissionSimTab'; import { useState, useEffect } from 'react'; import axios from 'axios'; import { debug, info, warn, error, logApiCall, logApiError, logUserAction } from './utils/logger'; function App() { const [tab, setTab] = useState('mission'); // Global login/session state const [authedPlayer, setAuthedPlayer] = useState(() => localStorage.getItem('dw:shop:authedPlayer') ? JSON.parse(localStorage.getItem('dw:shop:authedPlayer')) : ''); const [sessionId, setSessionId] = useState(() => localStorage.getItem('dw:shop:sessionId') ? JSON.parse(localStorage.getItem('dw:shop:sessionId')) : ''); const [loginName, setLoginName] = useState(''); const [loginPw, setLoginPw] = useState(''); const [loginMsg, setLoginMsg] = useState(''); const [players, setPlayers] = useState([]); const [playersError, setPlayersError] = useState(''); // Fetch player list for login (exposed so UI can refresh) async function fetchPlayers() { try { setPlayersError('') const res = await axios.get('/api/players/names'); setPlayers(res.data || []); } catch (err) { console.error('fetchPlayers failed', err); setPlayers([]); setPlayersError('Failed to load users — is the backend running? Check console/network.'); } } // Fetch players on mount useEffect(() => { fetchPlayers(); }, []); // Validate session on mount/refresh useEffect(() => { async function validate() { if (sessionId) { try { debug(`Validating session: ${sessionId}`, 'auth'); const res = await axios.post('/api/sessions/validate', { sessionId }); logApiCall('POST', '/api/sessions/validate', { sessionId }, res.status); if (res.data && res.data.playerName) { setAuthedPlayer(res.data.playerName); localStorage.setItem('dw:shop:authedPlayer', JSON.stringify(res.data.playerName)); // Fetch full player data for the validated session try { const fullPlayerResponse = await axios.get(`/api/players/${res.data.playerName}`, { headers: { 'x-session-id': sessionId } }); localStorage.setItem('dw:shop:playerData', JSON.stringify(fullPlayerResponse.data)); info(`Session validation and player data fetch successful for: ${res.data.playerName}`, 'auth'); } catch (playerFetchError) { warn(`Failed to fetch full player data during session validation: ${playerFetchError.message}`, 'auth'); } info(`Session validation successful for: ${res.data.playerName}`, 'auth'); } else { warn('Session validation failed - invalid response', 'auth'); setAuthedPlayer(''); setSessionId(''); localStorage.removeItem('dw:shop:authedPlayer'); localStorage.removeItem('dw:shop:sessionId'); } } catch (err) { logApiError('App', 'POST', '/api/sessions/validate', err); error(`Session validation error: ${err.message}`, 'auth'); setAuthedPlayer(''); setSessionId(''); localStorage.removeItem('dw:shop:authedPlayer'); localStorage.removeItem('dw:shop:sessionId'); } } else { debug('No session ID found for validation', 'auth'); } } validate(); }, [sessionId]); const handleKeyPress = (e) => { if (e.key === 'Enter') { handleLogin(); } }; const handleLogin = async () => { try { logUserAction('user', 'Login attempt', { username: loginName }); const response = await axios.post('/api/players/login', { name: loginName, password: loginPw }); logApiCall('POST', '/api/players/login', { name: loginName }, response.status); if (response.data.sessionId && response.data.player) { // Update global state with session info and player data setAuthedPlayer(response.data.player.name); setSessionId(response.data.sessionId); localStorage.setItem('dw:shop:authedPlayer', JSON.stringify(response.data.player.name)); localStorage.setItem('dw:shop:sessionId', JSON.stringify(response.data.sessionId)); // Fetch full player data including tabInfo after successful login try { const fullPlayerResponse = await axios.get(`/api/players/${response.data.player.name}`, { headers: { 'x-session-id': response.data.sessionId } }); localStorage.setItem('dw:shop:playerData', JSON.stringify(fullPlayerResponse.data)); info(`Full player data loaded for: ${loginName}`, 'auth'); } catch (playerFetchError) { warn(`Failed to fetch full player data: ${playerFetchError.message}`, 'auth'); // Store minimal player data as fallback localStorage.setItem('dw:shop:playerData', JSON.stringify(response.data.player)); } info(`Login successful for user: ${loginName}`, 'auth'); logUserAction('user', 'Login successful', { username: loginName }); setLoginMsg('Login successful!'); setLoginName(''); setLoginPw(''); // Clear success message after 3 seconds setTimeout(() => setLoginMsg(''), 3000); } else { warn(`Login failed for user: ${loginName} - ${response.data.message || 'Unknown error'}`, 'auth'); setLoginMsg(response.data.message || 'Login failed'); // Clear error message after 5 seconds setTimeout(() => setLoginMsg(''), 5000); } } catch (err) { logApiError('App', 'POST', '/api/players/login', err); error(`Login error for user: ${loginName} - ${err.message}`, 'auth'); setLoginMsg('Login failed. Please check your credentials and try again.'); // Clear error message after 5 seconds setTimeout(() => setLoginMsg(''), 5000); } }; async function handleLogout() { logUserAction('user', 'Logout attempt', { username: authedPlayer }); if (sessionId) { try { await axios.post('/api/sessions/logout', { sessionId }); info(`Logout successful for user: ${authedPlayer}`, 'auth'); } catch (err) { warn(`Logout API call failed: ${err.message}`, 'auth'); } } setAuthedPlayer(''); setSessionId(''); localStorage.removeItem('dw:shop:authedPlayer'); localStorage.removeItem('dw:shop:sessionId'); setLoginMsg('Logged out successfully'); info('User logged out successfully', 'auth'); // Clear logout message after 3 seconds setTimeout(() => setLoginMsg(''), 3000); } // GM session is now handled through the regular login const [appBackgroundUrl, setAppBackgroundUrl] = useState(() => { try { return localStorage.getItem('dw:gmkit:background') || '/gmkit/deathwatch-banner.png'; } catch (e) { return '/gmkit/deathwatch-banner.png'; } }); // On mount, try to get the gmkit list and pick a banner if available. useEffect(() => { let mounted = true; async function pickBackground() { try { const res = await axios.get('/api/gmkit/list'); const list = Array.isArray(res.data) ? res.data : []; if (!mounted) return; // prefer explicit deathwatch-banner.png if present const banner = list.find(f => f.name === 'deathwatch-banner.png'); if (banner) return setAppBackgroundUrl(banner.url); // otherwise use the first image-like file (png/jpg/jpeg) const firstImg = list.find(f => ['png', 'jpg', 'jpeg'].includes(f.ext)); if (firstImg) return setAppBackgroundUrl(firstImg.url); // fallback remains the default (may 404) or no background } catch (err) { // ignore — keep default console.debug('Could not load gmkit list for background', err.message || err); } } pickBackground(); return () => { mounted = false; }; }, []); const appBackgroundStyle = { backgroundImage: `linear-gradient(rgba(2,6,23,0.75), rgba(15,23,42,0.75)), url('${appBackgroundUrl}')`, backgroundRepeat: 'no-repeat', backgroundPosition: 'left center, right center', backgroundSize: '25%, 25%', backgroundAttachment: 'fixed', backgroundBlendMode: 'multiply' }; return (
Password for all users: 1234
The Bestiary is only accessible to Game Masters. Please log in with a GM account.
Mission Simulation is only accessible to Game Masters. Please log in with a GM account.