Skip to content

Commit c8887b6

Browse files
authored
Merge pull request #40 from CoolerMinecraft/UpdateACARSChartsDrawer
Fix Atis encryption, fix avatars, fix atis reminder location
2 parents d663ea9 + eaeb144 commit c8887b6

11 files changed

Lines changed: 147 additions & 68 deletions

File tree

server/routes/atis.ts

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import express from 'express';
22
import requireAuth from '../middleware/auth.js';
33
import { updateSession } from '../db/sessions.js';
4+
import { encrypt } from '../utils/encryption.js';
45

56
const router = express.Router();
67

@@ -85,19 +86,24 @@ router.post('/generate', requireAuth, async (req, res) => {
8586
const atisTimestamp = new Date().toISOString();
8687

8788
const atisData = {
88-
letter: ident,
89-
text: generatedAtis,
90-
timestamp: atisTimestamp,
89+
letter: ident,
90+
text: generatedAtis,
91+
timestamp: atisTimestamp,
9192
};
92-
const updatedSession = await updateSession(sessionId, { atis: JSON.stringify(atisData) });
93+
94+
const encryptedAtis = encrypt(atisData);
95+
const updatedSession = await updateSession(sessionId, { atis: JSON.stringify(encryptedAtis) });
9396
if (!updatedSession) {
9497
throw new Error('Failed to update session with ATIS data');
9598
}
9699

97100
res.json({
98-
atisText: generatedAtis,
99-
ident,
101+
text: generatedAtis,
102+
letter: ident,
100103
timestamp: atisTimestamp,
104+
// Backwards compatibility: include old field names
105+
atisText: generatedAtis,
106+
ident: ident,
101107
});
102108
} catch (error) {
103109
console.error('Error generating ATIS:', error);

server/routes/sessions.ts

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import { sanitizeAlphanumeric } from '../utils/sanitization.js';
1818
import { getUserById } from '../db/users.js';
1919
import { getUserRoles } from '../db/roles.js';
2020
import { isAdmin } from '../middleware/admin.js';
21+
import { encrypt, decrypt } from '../utils/encryption.js';
2122

2223
import { Request, Response } from 'express';
2324
import { JwtPayloadClient } from '../types/JwtPayload.js';
@@ -157,9 +158,10 @@ router.get('/:sessionId', requireSessionAccess, async (req, res) => {
157158
let atis = { letter: 'A', text: '', timestamp: new Date().toISOString() };
158159
if (session.atis) {
159160
try {
160-
atis = JSON.parse(session.atis);
161-
} catch (e) {
162-
console.log('parse error:', e);
161+
const parsed = JSON.parse(session.atis);
162+
atis = decrypt(parsed);
163+
} catch (err) {
164+
console.error('Error decrypting ATIS:', err);
163165
// fallback to default atis
164166
}
165167
}
@@ -184,16 +186,27 @@ router.put('/:sessionId', requireSessionAccess, async (req, res) => {
184186
try {
185187
const { sessionId } = req.params;
186188
const { activeRunway, atis } = req.body;
189+
let encryptedAtis = undefined;
190+
if (atis) {
191+
const encrypted = encrypt(atis);
192+
encryptedAtis = JSON.stringify(encrypted);
193+
}
194+
187195
// updateSession expects snake_case keys
188-
const session = await updateSession(sessionId, { active_runway: activeRunway, atis });
196+
const session = await updateSession(sessionId, {
197+
active_runway: activeRunway,
198+
atis: encryptedAtis
199+
});
189200
if (!session) {
190201
return res.status(404).json({ error: 'Session not found' });
191202
}
192203
let decryptedAtis = { letter: 'A', text: '', timestamp: new Date().toISOString() };
193204
if (session.atis) {
194205
try {
195-
decryptedAtis = JSON.parse(session.atis);
196-
} catch {
206+
const parsed = JSON.parse(session.atis);
207+
decryptedAtis = decrypt(parsed);
208+
} catch (err) {
209+
console.error('Error decrypting ATIS:', err);
197210
// fallback to default atis
198211
}
199212
}

server/websockets/overviewWebsocket.ts

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -101,11 +101,16 @@ export async function getOverviewData(sessionUsersIO: { activeUsers: Map<string,
101101
const controllers = await Promise.all(sessionUsers.map(async (user) => {
102102
let hasVatsimRating = false;
103103
let isEventController = false;
104+
let avatar = null;
104105

105106
if (user.id) {
106107
try {
107108
const userData = await getUserById(user.id);
108109
hasVatsimRating = userData?.vatsim_rating_id && userData.vatsim_rating_id > 1;
110+
111+
if (userData?.avatar) {
112+
avatar = `https://cdn.discordapp.com/avatars/${user.id}/${userData.avatar}.png`;
113+
}
109114

110115
if (user.roles) {
111116
isEventController = user.roles.some(role => role.name === 'Event Controller');
@@ -118,6 +123,7 @@ export async function getOverviewData(sessionUsersIO: { activeUsers: Map<string,
118123
return {
119124
username: user.username || 'Unknown',
120125
role: user.position || 'APP',
126+
avatar,
121127
hasVatsimRating,
122128
isEventController
123129
};
@@ -139,11 +145,27 @@ export async function getOverviewData(sessionUsersIO: { activeUsers: Map<string,
139145
} catch (error) {
140146
console.error(`Error fetching flights for session ${session.session_id}:`, error);
141147

142-
const controllers = sessionUsers.map(user => ({
143-
username: user.username || 'Unknown',
144-
role: user.position || 'APP',
145-
hasVatsimRating: false,
146-
isEventController: false
148+
const controllers = await Promise.all(sessionUsers.map(async (user) => {
149+
let avatar = null;
150+
151+
if (user.id) {
152+
try {
153+
const userData = await getUserById(user.id);
154+
if (userData?.avatar) {
155+
avatar = `https://cdn.discordapp.com/avatars/${user.id}/${userData.avatar}.png`;
156+
}
157+
} catch (err) {
158+
// Ignore avatar fetch errors in fallback
159+
}
160+
}
161+
162+
return {
163+
username: user.username || 'Unknown',
164+
role: user.position || 'APP',
165+
avatar,
166+
hasVatsimRating: false,
167+
isEventController: false
168+
};
147169
}));
148170

149171
activeSessions.push({

server/websockets/sessionUsersWebsocket.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { validateSessionId, validateAccessId } from '../utils/validation.js';
77
import type { Server as HttpServer } from 'http';
88
import { incrementStat } from '../utils/statisticsCache.js';
99
import { getOverviewIO } from './overviewWebsocket.js';
10+
import { encrypt, decrypt } from '../utils/encryption.js';
1011

1112
const activeUsers = new Map();
1213
const sessionATISConfigs = new Map();
@@ -37,7 +38,8 @@ async function generateAutoATIS(sessionId: string, config: ATISConfig, io: Socke
3738
const session = await getSessionById(sessionId);
3839
if (!session?.atis) return;
3940

40-
const currentAtis = JSON.parse(session.atis);
41+
const storedAtis = JSON.parse(session.atis);
42+
const currentAtis = decrypt(storedAtis);
4143
const currentLetter = currentAtis.letter || 'A';
4244
const identOptions = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'.split('');
4345
const currentIndex = identOptions.indexOf(currentLetter);
@@ -117,7 +119,8 @@ async function generateAutoATIS(sessionId: string, config: ATISConfig, io: Socke
117119
timestamp: new Date().toISOString(),
118120
};
119121

120-
await updateSession(sessionId, { atis: JSON.stringify(atisData) });
122+
const encryptedAtis = encrypt(atisData);
123+
await updateSession(sessionId, { atis: JSON.stringify(encryptedAtis) });
121124

122125
io.to(sessionId).emit('atisUpdate', {
123126
atis: atisData,
@@ -309,15 +312,18 @@ export function setupSessionUsersWebsocket(httpServer: HttpServer) {
309312
try {
310313
const session = await getSessionById(sessionId);
311314
if (session?.atis) {
312-
socket.emit('atisUpdate', JSON.parse(session.atis));
315+
const encryptedAtis = JSON.parse(session.atis);
316+
const decryptedAtis = decrypt(encryptedAtis);
317+
socket.emit('atisUpdate', decryptedAtis);
313318
}
314319
} catch (error) {
315320
console.error('Error sending ATIS data:', error);
316321
}
317322

318323
socket.on('atisGenerated', async (atisData) => {
319324
try {
320-
await updateSession(sessionId, { atis: atisData.atis });
325+
const encryptedAtis = encrypt(atisData.atis);
326+
await updateSession(sessionId, { atis: JSON.stringify(encryptedAtis) });
321327

322328
sessionATISConfigs.set(sessionId, {
323329
icao: atisData.icao,

src/components/acars/AcarsChartDrawer.tsx

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useState } from 'react';
1+
import { useState, useEffect } from 'react';
22
import { Map, ZoomIn, ZoomOut, X, ArrowLeft, Plane, PlaneLanding, ChevronDown, ChevronUp, Loader2 } from 'lucide-react';
33
import type { Airport } from '../../types/airports';
44
import type { Settings } from '../../types/settings';
@@ -79,6 +79,12 @@ export default function AcarsChartDrawer({
7979

8080
const chartsToUse = isLegacyMode ? allChartsForLegacy : charts;
8181

82+
useEffect(() => {
83+
if (selectedChart) {
84+
setImageLoading(true);
85+
}
86+
}, [selectedChart]);
87+
8288
return (
8389
<div
8490
className={`fixed bottom-0 left-0 right-0 bg-zinc-900 text-white transition-transform duration-300 ${
@@ -334,7 +340,6 @@ export default function AcarsChartDrawer({
334340
}}
335341
draggable={false}
336342
onDragStart={(e) => e.preventDefault()}
337-
onLoadStart={() => setImageLoading(true)}
338343
onLoad={(e) => {
339344
setChartLoadError(false);
340345
setImageLoading(false);
@@ -434,7 +439,6 @@ export default function AcarsChartDrawer({
434439
}}
435440
draggable={false}
436441
onDragStart={(e) => e.preventDefault()}
437-
onLoadStart={() => setImageLoading(true)}
438442
onLoad={(e) => {
439443
setChartLoadError(false);
440444
setImageLoading(false);

src/components/acars/AcarsSidebar.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,9 @@ export default function AcarsSidebar({
4646
)
4747
}
4848
src={
49-
controller.username === user?.username
49+
controller.avatar
50+
? getAvatarUrl(controller.avatar)
51+
: controller.username === user?.username
5052
? getAvatarUrl(user.avatar)
5153
: getAvatarUrl(null)
5254
}

src/components/acars/AcarsTerminal.tsx

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,18 @@ export default function AcarsTerminal({
3232
<div className="flex-1 overflow-y-auto p-4 font-mono text-xs space-y-1.5 bg-black">
3333
{messages.map((msg) => (
3434
<div key={msg.id} className={getMessageColor(msg.type)}>
35-
{renderMessageText(msg)}
35+
<div className="flex gap-2 mb-0.5">
36+
<span className="text-zinc-500">
37+
{new Date(msg.timestamp).toLocaleTimeString('en-US', {
38+
hour12: false,
39+
hour: '2-digit',
40+
minute: '2-digit',
41+
timeZone: 'UTC'
42+
})}Z
43+
</span>
44+
<span className="text-zinc-400">[{msg.station}]:</span>
45+
<div>{renderMessageText(msg)}</div>
46+
</div>
3647
</div>
3748
))}
3849
<div ref={messagesEndRef} />

src/components/modals/AtisReminderModal.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ export default function AtisReminderModal({ onContinue, atisText, sessionId, use
3232

3333
return (
3434
<div className="fixed inset-0 bg-black/80 backdrop-blur-sm flex items-center justify-center z-50 p-4">
35-
<div className="bg-slate-900 border-2 border-blue-500/50 rounded-2xl p-8 max-w-2xl w-full">
35+
<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">
3636
<h2 className="text-3xl font-bold text-blue-400 mb-4">
3737
PFATC Network ATIS Format Reminder
3838
</h2>

src/pages/Create.tsx

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,12 @@ import RunwayDropdown from '../components/dropdowns/RunwayDropdown';
1010
import Checkbox from '../components/common/Checkbox';
1111
import Button from '../components/common/Button';
1212
import WindDisplay from '../components/tools/WindDisplay';
13+
import AtisReminderModal from '../components/modals/AtisReminderModal';
1314
import Joyride, { type CallBackProps, STATUS } from 'react-joyride';
1415
import CustomTooltip from '../components/tutorial/CustomTooltip';
1516
import { updateTutorialStatus } from '../utils/fetch/auth';
1617
import { steps } from '../components/tutorial/TutorialStepsCreate';
18+
import { useData } from '../hooks/data/useData';
1719

1820
export default function Create() {
1921
const navigate = useNavigate();
@@ -26,7 +28,10 @@ export default function Create() {
2628
const [sessionLimitReached, setSessionLimitReached] =
2729
useState<boolean>(false);
2830
const [isDeletingOldest, setIsDeletingOldest] = useState<boolean>(false);
31+
const [showAtisReminderModal, setShowAtisReminderModal] = useState(false);
32+
const [createdSession, setCreatedSession] = useState<{ sessionId: string; accessId: string; atisText: string } | null>(null);
2933
const { user } = useAuth();
34+
const { airports, frequencies } = useData();
3035
const [searchParams] = useSearchParams();
3136
const startTutorial = searchParams.get('tutorial') === 'true';
3237

@@ -92,15 +97,24 @@ export default function Create() {
9297

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

95-
await generateATIS({
100+
const atisResponse = await generateATIS({
96101
sessionId: newSession.sessionId,
97102
ident: 'A',
98103
icao: selectedAirport,
99104
landing_runways: [selectedRunway],
100105
departing_runways: [selectedRunway],
101106
});
102107

103-
handleContinueToSession(newSession.sessionId, newSession.accessId);
108+
if (isPFATCNetwork && atisResponse?.atisText) {
109+
setCreatedSession({
110+
sessionId: newSession.sessionId,
111+
accessId: newSession.accessId,
112+
atisText: atisResponse.atisText
113+
});
114+
setShowAtisReminderModal(true);
115+
} else {
116+
handleContinueToSession(newSession.sessionId, newSession.accessId);
117+
}
104118
} catch (err) {
105119
console.error('Error creating session:', err);
106120
const errorMessage =
@@ -319,6 +333,34 @@ export default function Create() {
319333
},
320334
}}
321335
/>
336+
337+
{/* ATIS Reminder Modal */}
338+
{showAtisReminderModal && createdSession && user && (
339+
<AtisReminderModal
340+
onContinue={() => {
341+
setShowAtisReminderModal(false);
342+
handleContinueToSession(createdSession.sessionId, createdSession.accessId);
343+
}}
344+
atisText={createdSession.atisText}
345+
accessId={createdSession.accessId}
346+
userId={user.userId}
347+
sessionId={createdSession.sessionId}
348+
airportIcao={selectedAirport}
349+
airportName={
350+
airports.find((a) => a.icao === selectedAirport)?.name ||
351+
selectedAirport
352+
}
353+
airportControlName={
354+
airports.find((a) => a.icao === selectedAirport)?.controlName ||
355+
selectedAirport
356+
}
357+
airportAppFrequency={
358+
airports.find((a) => a.icao === selectedAirport)?.allFrequencies?.APP ||
359+
frequencies.find((f) => f.icao === selectedAirport)?.APP ||
360+
'---'
361+
}
362+
/>
363+
)}
322364
</div>
323365
);
324366
}

0 commit comments

Comments
 (0)