diff --git a/server/routes/atis.ts b/server/routes/atis.ts index 1170622..b8f8fff 100644 --- a/server/routes/atis.ts +++ b/server/routes/atis.ts @@ -1,6 +1,7 @@ import express from 'express'; import requireAuth from '../middleware/auth.js'; import { updateSession } from '../db/sessions.js'; +import { encrypt } from '../utils/encryption.js'; const router = express.Router(); @@ -85,19 +86,24 @@ router.post('/generate', requireAuth, async (req, res) => { const atisTimestamp = new Date().toISOString(); const atisData = { - letter: ident, - text: generatedAtis, - timestamp: atisTimestamp, + letter: ident, + text: generatedAtis, + timestamp: atisTimestamp, }; - const updatedSession = await updateSession(sessionId, { atis: JSON.stringify(atisData) }); + + const encryptedAtis = encrypt(atisData); + const updatedSession = await updateSession(sessionId, { atis: JSON.stringify(encryptedAtis) }); if (!updatedSession) { throw new Error('Failed to update session with ATIS data'); } res.json({ - atisText: generatedAtis, - ident, + text: generatedAtis, + letter: ident, timestamp: atisTimestamp, + // Backwards compatibility: include old field names + atisText: generatedAtis, + ident: ident, }); } catch (error) { console.error('Error generating ATIS:', error); diff --git a/server/routes/sessions.ts b/server/routes/sessions.ts index 5ece1ed..2d63014 100644 --- a/server/routes/sessions.ts +++ b/server/routes/sessions.ts @@ -18,6 +18,7 @@ import { sanitizeAlphanumeric } from '../utils/sanitization.js'; import { getUserById } from '../db/users.js'; import { getUserRoles } from '../db/roles.js'; import { isAdmin } from '../middleware/admin.js'; +import { encrypt, decrypt } from '../utils/encryption.js'; import { Request, Response } from 'express'; import { JwtPayloadClient } from '../types/JwtPayload.js'; @@ -157,9 +158,10 @@ router.get('/:sessionId', requireSessionAccess, async (req, res) => { let atis = { letter: 'A', text: '', timestamp: new Date().toISOString() }; if (session.atis) { try { - atis = JSON.parse(session.atis); - } catch (e) { - console.log('parse error:', e); + const parsed = JSON.parse(session.atis); + atis = decrypt(parsed); + } catch (err) { + console.error('Error decrypting ATIS:', err); // fallback to default atis } } @@ -184,16 +186,27 @@ router.put('/:sessionId', requireSessionAccess, async (req, res) => { try { const { sessionId } = req.params; const { activeRunway, atis } = req.body; + let encryptedAtis = undefined; + if (atis) { + const encrypted = encrypt(atis); + encryptedAtis = JSON.stringify(encrypted); + } + // updateSession expects snake_case keys - const session = await updateSession(sessionId, { active_runway: activeRunway, atis }); + const session = await updateSession(sessionId, { + active_runway: activeRunway, + atis: encryptedAtis + }); if (!session) { return res.status(404).json({ error: 'Session not found' }); } let decryptedAtis = { letter: 'A', text: '', timestamp: new Date().toISOString() }; if (session.atis) { try { - decryptedAtis = JSON.parse(session.atis); - } catch { + const parsed = JSON.parse(session.atis); + decryptedAtis = decrypt(parsed); + } catch (err) { + console.error('Error decrypting ATIS:', err); // fallback to default atis } } diff --git a/server/websockets/overviewWebsocket.ts b/server/websockets/overviewWebsocket.ts index cc55d82..88874f1 100644 --- a/server/websockets/overviewWebsocket.ts +++ b/server/websockets/overviewWebsocket.ts @@ -101,11 +101,16 @@ export async function getOverviewData(sessionUsersIO: { activeUsers: Map { let hasVatsimRating = false; let isEventController = false; + let avatar = null; if (user.id) { try { const userData = await getUserById(user.id); hasVatsimRating = userData?.vatsim_rating_id && userData.vatsim_rating_id > 1; + + if (userData?.avatar) { + avatar = `https://cdn.discordapp.com/avatars/${user.id}/${userData.avatar}.png`; + } if (user.roles) { isEventController = user.roles.some(role => role.name === 'Event Controller'); @@ -118,6 +123,7 @@ export async function getOverviewData(sessionUsersIO: { activeUsers: Map ({ - username: user.username || 'Unknown', - role: user.position || 'APP', - hasVatsimRating: false, - isEventController: false + const controllers = await Promise.all(sessionUsers.map(async (user) => { + let avatar = null; + + if (user.id) { + try { + const userData = await getUserById(user.id); + if (userData?.avatar) { + avatar = `https://cdn.discordapp.com/avatars/${user.id}/${userData.avatar}.png`; + } + } catch (err) { + // Ignore avatar fetch errors in fallback + } + } + + return { + username: user.username || 'Unknown', + role: user.position || 'APP', + avatar, + hasVatsimRating: false, + isEventController: false + }; })); activeSessions.push({ diff --git a/server/websockets/sessionUsersWebsocket.ts b/server/websockets/sessionUsersWebsocket.ts index 3c37cbd..1c6f36f 100644 --- a/server/websockets/sessionUsersWebsocket.ts +++ b/server/websockets/sessionUsersWebsocket.ts @@ -7,6 +7,7 @@ import { validateSessionId, validateAccessId } from '../utils/validation.js'; import type { Server as HttpServer } from 'http'; import { incrementStat } from '../utils/statisticsCache.js'; import { getOverviewIO } from './overviewWebsocket.js'; +import { encrypt, decrypt } from '../utils/encryption.js'; const activeUsers = new Map(); const sessionATISConfigs = new Map(); @@ -37,7 +38,8 @@ async function generateAutoATIS(sessionId: string, config: ATISConfig, io: Socke const session = await getSessionById(sessionId); if (!session?.atis) return; - const currentAtis = JSON.parse(session.atis); + const storedAtis = JSON.parse(session.atis); + const currentAtis = decrypt(storedAtis); const currentLetter = currentAtis.letter || 'A'; const identOptions = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'.split(''); const currentIndex = identOptions.indexOf(currentLetter); @@ -117,7 +119,8 @@ async function generateAutoATIS(sessionId: string, config: ATISConfig, io: Socke timestamp: new Date().toISOString(), }; - await updateSession(sessionId, { atis: JSON.stringify(atisData) }); + const encryptedAtis = encrypt(atisData); + await updateSession(sessionId, { atis: JSON.stringify(encryptedAtis) }); io.to(sessionId).emit('atisUpdate', { atis: atisData, @@ -309,7 +312,9 @@ export function setupSessionUsersWebsocket(httpServer: HttpServer) { try { const session = await getSessionById(sessionId); if (session?.atis) { - socket.emit('atisUpdate', JSON.parse(session.atis)); + const encryptedAtis = JSON.parse(session.atis); + const decryptedAtis = decrypt(encryptedAtis); + socket.emit('atisUpdate', decryptedAtis); } } catch (error) { console.error('Error sending ATIS data:', error); @@ -317,7 +322,8 @@ export function setupSessionUsersWebsocket(httpServer: HttpServer) { socket.on('atisGenerated', async (atisData) => { try { - await updateSession(sessionId, { atis: atisData.atis }); + const encryptedAtis = encrypt(atisData.atis); + await updateSession(sessionId, { atis: JSON.stringify(encryptedAtis) }); sessionATISConfigs.set(sessionId, { icao: atisData.icao, diff --git a/src/components/acars/AcarsChartDrawer.tsx b/src/components/acars/AcarsChartDrawer.tsx index 95b9df9..d15806c 100644 --- a/src/components/acars/AcarsChartDrawer.tsx +++ b/src/components/acars/AcarsChartDrawer.tsx @@ -1,4 +1,4 @@ -import { useState } from 'react'; +import { useState, useEffect } from 'react'; import { Map, ZoomIn, ZoomOut, X, ArrowLeft, Plane, PlaneLanding, ChevronDown, ChevronUp, Loader2 } from 'lucide-react'; import type { Airport } from '../../types/airports'; import type { Settings } from '../../types/settings'; @@ -79,6 +79,12 @@ export default function AcarsChartDrawer({ const chartsToUse = isLegacyMode ? allChartsForLegacy : charts; + useEffect(() => { + if (selectedChart) { + setImageLoading(true); + } + }, [selectedChart]); + return (
e.preventDefault()} - onLoadStart={() => setImageLoading(true)} onLoad={(e) => { setChartLoadError(false); setImageLoading(false); @@ -434,7 +439,6 @@ export default function AcarsChartDrawer({ }} draggable={false} onDragStart={(e) => e.preventDefault()} - onLoadStart={() => setImageLoading(true)} onLoad={(e) => { setChartLoadError(false); setImageLoading(false); diff --git a/src/components/acars/AcarsSidebar.tsx b/src/components/acars/AcarsSidebar.tsx index 5ff65e2..cb231f1 100644 --- a/src/components/acars/AcarsSidebar.tsx +++ b/src/components/acars/AcarsSidebar.tsx @@ -46,7 +46,9 @@ export default function AcarsSidebar({ ) } src={ - controller.username === user?.username + controller.avatar + ? getAvatarUrl(controller.avatar) + : controller.username === user?.username ? getAvatarUrl(user.avatar) : getAvatarUrl(null) } diff --git a/src/components/acars/AcarsTerminal.tsx b/src/components/acars/AcarsTerminal.tsx index b314af6..b71bf21 100644 --- a/src/components/acars/AcarsTerminal.tsx +++ b/src/components/acars/AcarsTerminal.tsx @@ -32,7 +32,18 @@ export default function AcarsTerminal({
{messages.map((msg) => (
- {renderMessageText(msg)} +
+ + {new Date(msg.timestamp).toLocaleTimeString('en-US', { + hour12: false, + hour: '2-digit', + minute: '2-digit', + timeZone: 'UTC' + })}Z + + [{msg.station}]: +
{renderMessageText(msg)}
+
))}
diff --git a/src/components/modals/AtisReminderModal.tsx b/src/components/modals/AtisReminderModal.tsx index 9fb80e6..5279fc5 100644 --- a/src/components/modals/AtisReminderModal.tsx +++ b/src/components/modals/AtisReminderModal.tsx @@ -32,7 +32,7 @@ export default function AtisReminderModal({ onContinue, atisText, sessionId, use return (
-
+

PFATC Network ATIS Format Reminder

diff --git a/src/pages/Create.tsx b/src/pages/Create.tsx index 1cd8f10..d454a22 100644 --- a/src/pages/Create.tsx +++ b/src/pages/Create.tsx @@ -10,10 +10,12 @@ import RunwayDropdown from '../components/dropdowns/RunwayDropdown'; import Checkbox from '../components/common/Checkbox'; import Button from '../components/common/Button'; import WindDisplay from '../components/tools/WindDisplay'; +import AtisReminderModal from '../components/modals/AtisReminderModal'; import Joyride, { type CallBackProps, STATUS } from 'react-joyride'; import CustomTooltip from '../components/tutorial/CustomTooltip'; import { updateTutorialStatus } from '../utils/fetch/auth'; import { steps } from '../components/tutorial/TutorialStepsCreate'; +import { useData } from '../hooks/data/useData'; export default function Create() { const navigate = useNavigate(); @@ -26,7 +28,10 @@ export default function Create() { const [sessionLimitReached, setSessionLimitReached] = useState(false); const [isDeletingOldest, setIsDeletingOldest] = useState(false); + const [showAtisReminderModal, setShowAtisReminderModal] = useState(false); + const [createdSession, setCreatedSession] = useState<{ sessionId: string; accessId: string; atisText: string } | null>(null); const { user } = useAuth(); + const { airports, frequencies } = useData(); const [searchParams] = useSearchParams(); const startTutorial = searchParams.get('tutorial') === 'true'; @@ -92,7 +97,7 @@ export default function Create() { setSessionCount((prev) => prev + 1); - await generateATIS({ + const atisResponse = await generateATIS({ sessionId: newSession.sessionId, ident: 'A', icao: selectedAirport, @@ -100,7 +105,16 @@ export default function Create() { departing_runways: [selectedRunway], }); - handleContinueToSession(newSession.sessionId, newSession.accessId); + if (isPFATCNetwork && atisResponse?.atisText) { + setCreatedSession({ + sessionId: newSession.sessionId, + accessId: newSession.accessId, + atisText: atisResponse.atisText + }); + setShowAtisReminderModal(true); + } else { + handleContinueToSession(newSession.sessionId, newSession.accessId); + } } catch (err) { console.error('Error creating session:', err); const errorMessage = @@ -319,6 +333,34 @@ export default function Create() { }, }} /> + + {/* ATIS Reminder Modal */} + {showAtisReminderModal && createdSession && user && ( + { + setShowAtisReminderModal(false); + handleContinueToSession(createdSession.sessionId, createdSession.accessId); + }} + atisText={createdSession.atisText} + accessId={createdSession.accessId} + userId={user.userId} + sessionId={createdSession.sessionId} + airportIcao={selectedAirport} + airportName={ + airports.find((a) => a.icao === selectedAirport)?.name || + selectedAirport + } + airportControlName={ + airports.find((a) => a.icao === selectedAirport)?.controlName || + selectedAirport + } + airportAppFrequency={ + airports.find((a) => a.icao === selectedAirport)?.allFrequencies?.APP || + frequencies.find((f) => f.icao === selectedAirport)?.APP || + '---' + } + /> + )}
); } diff --git a/src/pages/Flights.tsx b/src/pages/Flights.tsx index f004260..6965fc6 100644 --- a/src/pages/Flights.tsx +++ b/src/pages/Flights.tsx @@ -26,13 +26,11 @@ import ArrivalsTable from '../components/tables/ArrivalsTable'; import CombinedFlightsTable from '../components/tables/CombinedFlightsTable'; import AccessDenied from '../components/AccessDenied'; import AddCustomFlightModal from '../components/modals/AddCustomFlightModal'; -import AtisReminderModal from '../components/modals/AtisReminderModal'; import ContactAcarsSidebar from '../components/tools/ContactAcarsSidebar'; import Button from '../components/common/Button'; import Loader from '../components/common/Loader'; import Joyride, { type CallBackProps, STATUS } from 'react-joyride'; import CustomTooltip from '../components/tutorial/CustomTooltip'; -import { useData } from '../hooks/data/useData'; const API_BASE_URL = import.meta.env.VITE_SERVER_URL; @@ -102,7 +100,6 @@ export default function Flights() { const [showAddDepartureModal, setShowAddDepartureModal] = useState(false); const [showAddArrivalModal, setShowAddArrivalModal] = useState(false); const [showContactAcarsModal, setShowContactAcarsModal] = useState(false); - const [showAtisReminderModal, setShowAtisReminderModal] = useState(false); const [activeAcarsFlights, setActiveAcarsFlights] = useState< Set >(new Set()); @@ -110,8 +107,6 @@ export default function Flights() { [] ); - const { airports, frequencies } = useData(); - const userRef = useRef(user); const settingsRef = useRef(settings); const flightsSocketConnectedRef = useRef(false); @@ -181,13 +176,6 @@ export default function Flights() { setAccessError(null); }, [sessionId, accessId]); - useEffect(() => { - if (session?.isPFATC && session.atis?.text && initialLoadComplete) { - const timerId = setTimeout(() => setShowAtisReminderModal(true), 500); - return () => clearTimeout(timerId); - } - }, [session?.isPFATC, session?.atis?.text, initialLoadComplete]); - useEffect(() => { if ( !sessionId || @@ -649,12 +637,22 @@ export default function Flights() { })); // Combine regular flights with custom flights - return [...regularDepartures, ...customDepartureFlights]; + let allDepartures = [...regularDepartures, ...customDepartureFlights]; + + if (position !== 'ALL') { + const allowedStatuses = getAllowedStatuses(position); + allDepartures = allDepartures.filter((flight) => + allowedStatuses.includes(flight.status || '') + ); + } + + return allDepartures; }, [ flights, session?.airportIcao, localHiddenFlights, customDepartureFlights, + position, ]); const arrivalFlights = useMemo(() => { @@ -1115,32 +1113,6 @@ export default function Flights() { }, }} /> - - {/* ATIS Reminder Modal */} - {showAtisReminderModal && session && user && ( - setShowAtisReminderModal(false)} - atisText={session.atis?.text || ''} - accessId={accessId || ''} - userId={user.userId} - sessionId={sessionId || ''} - airportIcao={session.airportIcao} - airportName={ - airports.find((a) => a.icao === session.airportIcao)?.name || - session.airportIcao - } - airportControlName={ - airports.find((a) => a.icao === session.airportIcao)?.controlName || - session.airportIcao - } - airportAppFrequency={ - airports.find((a) => a.icao === session.airportIcao)?.allFrequencies - ?.APP || - frequencies.find((f) => f.icao === session.airportIcao)?.APP || - '---' - } - /> - )}
); } diff --git a/src/sockets/overviewSocket.ts b/src/sockets/overviewSocket.ts index 54826ed..169407d 100644 --- a/src/sockets/overviewSocket.ts +++ b/src/sockets/overviewSocket.ts @@ -14,6 +14,7 @@ export interface OverviewSession { controllers?: Array<{ username: string; role: string; + avatar?: string | null; hasVatsimRating?: boolean; isEventController?: boolean; }>;