Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 12 additions & 6 deletions server/routes/atis.ts
Original file line number Diff line number Diff line change
@@ -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();

Expand Down Expand Up @@ -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,
Comment thread
1ceit marked this conversation as resolved.
// Backwards compatibility: include old field names
atisText: generatedAtis,
ident: ident,
});
} catch (error) {
console.error('Error generating ATIS:', error);
Expand Down
25 changes: 19 additions & 6 deletions server/routes/sessions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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
}
}
Expand All @@ -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
}
}
Expand Down
32 changes: 27 additions & 5 deletions server/websockets/overviewWebsocket.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,11 +101,16 @@ export async function getOverviewData(sessionUsersIO: { activeUsers: Map<string,
const controllers = await Promise.all(sessionUsers.map(async (user) => {
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');
Expand All @@ -118,6 +123,7 @@ export async function getOverviewData(sessionUsersIO: { activeUsers: Map<string,
return {
username: user.username || 'Unknown',
role: user.position || 'APP',
avatar,
hasVatsimRating,
isEventController
};
Expand All @@ -139,11 +145,27 @@ export async function getOverviewData(sessionUsersIO: { activeUsers: Map<string,
} catch (error) {
console.error(`Error fetching flights for session ${session.session_id}:`, error);

const controllers = sessionUsers.map(user => ({
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({
Expand Down
14 changes: 10 additions & 4 deletions server/websockets/sessionUsersWebsocket.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -309,15 +312,18 @@ 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);
}

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,
Expand Down
10 changes: 7 additions & 3 deletions src/components/acars/AcarsChartDrawer.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -79,6 +79,12 @@ export default function AcarsChartDrawer({

const chartsToUse = isLegacyMode ? allChartsForLegacy : charts;

useEffect(() => {
if (selectedChart) {
setImageLoading(true);
}
}, [selectedChart]);

return (
<div
className={`fixed bottom-0 left-0 right-0 bg-zinc-900 text-white transition-transform duration-300 ${
Expand Down Expand Up @@ -334,7 +340,6 @@ export default function AcarsChartDrawer({
}}
draggable={false}
onDragStart={(e) => e.preventDefault()}
onLoadStart={() => setImageLoading(true)}
onLoad={(e) => {
setChartLoadError(false);
setImageLoading(false);
Expand Down Expand Up @@ -434,7 +439,6 @@ export default function AcarsChartDrawer({
}}
draggable={false}
onDragStart={(e) => e.preventDefault()}
onLoadStart={() => setImageLoading(true)}
onLoad={(e) => {
setChartLoadError(false);
setImageLoading(false);
Expand Down
4 changes: 3 additions & 1 deletion src/components/acars/AcarsSidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
13 changes: 12 additions & 1 deletion src/components/acars/AcarsTerminal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,18 @@ export default function AcarsTerminal({
<div className="flex-1 overflow-y-auto p-4 font-mono text-xs space-y-1.5 bg-black">
{messages.map((msg) => (
<div key={msg.id} className={getMessageColor(msg.type)}>
{renderMessageText(msg)}
<div className="flex gap-2 mb-0.5">
<span className="text-zinc-500">
{new Date(msg.timestamp).toLocaleTimeString('en-US', {
hour12: false,
hour: '2-digit',
minute: '2-digit',
timeZone: 'UTC'
})}Z
</span>
<span className="text-zinc-400">[{msg.station}]:</span>
<div>{renderMessageText(msg)}</div>
</div>
</div>
))}
<div ref={messagesEndRef} />
Expand Down
2 changes: 1 addition & 1 deletion src/components/modals/AtisReminderModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export default function AtisReminderModal({ onContinue, atisText, sessionId, use

return (
<div className="fixed inset-0 bg-black/80 backdrop-blur-sm flex items-center justify-center z-50 p-4">
<div className="bg-slate-900 border-2 border-blue-500/50 rounded-2xl p-8 max-w-2xl w-full">
<div className="bg-gradient-to-b from-slate-900 to-slate-950 border-2 border-zinc-500/50 rounded-2xl p-8 max-w-2xl w-full">
<h2 className="text-3xl font-bold text-blue-400 mb-4">
PFATC Network ATIS Format Reminder
</h2>
Expand Down
46 changes: 44 additions & 2 deletions src/pages/Create.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -26,7 +28,10 @@ export default function Create() {
const [sessionLimitReached, setSessionLimitReached] =
useState<boolean>(false);
const [isDeletingOldest, setIsDeletingOldest] = useState<boolean>(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';

Expand Down Expand Up @@ -92,15 +97,24 @@ export default function Create() {

setSessionCount((prev) => prev + 1);

await generateATIS({
const atisResponse = await generateATIS({
sessionId: newSession.sessionId,
ident: 'A',
icao: selectedAirport,
landing_runways: [selectedRunway],
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 =
Expand Down Expand Up @@ -319,6 +333,34 @@ export default function Create() {
},
}}
/>

{/* ATIS Reminder Modal */}
{showAtisReminderModal && createdSession && user && (
<AtisReminderModal
onContinue={() => {
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 ||
'---'
}
/>
)}
</div>
);
}
Loading
Loading