Skip to content

Commit 46737d8

Browse files
Merge pull request #3 from sebastiankrll/feature/pilot-panel
Feature/pilot panel
2 parents 730bcc4 + 2b1d8e6 commit 46737d8

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

45 files changed

+3015
-5410
lines changed

apps/api/src/index.ts

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -56,12 +56,12 @@ app.get("/data/init", async (_req, res) => {
5656
}
5757
});
5858

59-
app.get("/data/pilot/:callsign", async (req, res) => {
59+
app.get("/api/data/pilot/:id", async (req, res) => {
6060
try {
61-
const { callsign } = req.params;
62-
console.log("Requested pilot:", callsign);
61+
const { id } = req.params;
62+
console.log("Requested pilot:", id);
6363

64-
const pilot = await rdsGetSingle(`pilot:${callsign}`);
64+
const pilot = await rdsGetSingle(`pilot:${id}`);
6565
if (!pilot) return res.status(404).json({ error: "Pilot not found" });
6666

6767
res.json(pilot);
@@ -115,6 +115,21 @@ app.get("/data/track/:id", async (req, res) => {
115115
}
116116
});
117117

118+
app.get("/api/data/aircraft/:reg", async (req, res) => {
119+
try {
120+
const { reg } = req.params;
121+
console.log("Requested aircraft:", reg);
122+
123+
const aircraft = await rdsGetSingle(`fleet:${reg}`);
124+
if (!aircraft) return res.status(404).json({ error: "Aircraft not found" });
125+
126+
res.json(aircraft);
127+
} catch (err) {
128+
console.error(err);
129+
res.status(500).json({ error: "Internal server error" });
130+
}
131+
});
132+
118133
const PORT = process.env.API_PORT || 3001;
119134
app.listen(PORT, () => {
120135
console.log(`Express API listening on port ${PORT}`);

apps/ingestion/src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ async function fetchVatsimData(): Promise<void> {
4949
rdsSetSingle("ws:all", all);
5050

5151
// Set pilots, controllers and airports data in redis
52-
rdsSetMultiple(pilotsLong, "pilot", (p) => p.callsign, "pilots:live", 120);
52+
rdsSetMultiple(pilotsLong, "pilot", (p) => p.id, "pilots:live", 120);
5353
rdsSetMultiple(controllersLong, "controller", (c) => c.callsign, "controllers:live", 120);
5454
rdsSetMultiple(airportsLong, "airport", (a) => a.icao, "airports:live", 120);
5555

apps/ingestion/src/pilot.ts

Lines changed: 94 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { rdsGetMultiple } from "@sk/db/redis";
1+
import { rdsGetMultiple, rdsGetSingle } from "@sk/db/redis";
22
import type { StaticAirport } from "@sk/types/db";
33
import type {
44
PilotDelta,
@@ -13,6 +13,71 @@ import type {
1313
import { haversineDistance } from "./utils/helpers.js";
1414

1515
const TAXI_TIME_MS = 5 * 60 * 1000;
16+
const PILOT_RATINGS = [
17+
{
18+
id: 0,
19+
short_name: "NEW",
20+
long_name: "Basic Member",
21+
},
22+
{
23+
id: 1,
24+
short_name: "PPL",
25+
long_name: "Private Pilot License",
26+
},
27+
{
28+
id: 3,
29+
short_name: "IR",
30+
long_name: "Instrument Rating",
31+
},
32+
{
33+
id: 7,
34+
short_name: "CMEL",
35+
long_name: "Commercial Multi-Engine License",
36+
},
37+
{
38+
id: 15,
39+
short_name: "ATPL",
40+
long_name: "Airline Transport Pilot License",
41+
},
42+
{
43+
id: 31,
44+
short_name: "FI",
45+
long_name: "Flight Instructor",
46+
},
47+
{
48+
id: 63,
49+
short_name: "FE",
50+
long_name: "Flight Examiner",
51+
},
52+
];
53+
const MILITARY_RATINGS = [
54+
{
55+
id: 0,
56+
short_name: "M0",
57+
long_name: "No Military Rating",
58+
},
59+
{
60+
id: 1,
61+
short_name: "M1",
62+
long_name: "Military Pilot License",
63+
},
64+
{
65+
id: 3,
66+
short_name: "M2",
67+
long_name: "Military Instrument Rating",
68+
},
69+
{
70+
id: 7,
71+
short_name: "M3",
72+
long_name: "Military Multi-Engine Rating",
73+
},
74+
{
75+
id: 15,
76+
short_name: "M4",
77+
long_name: "Military Mission Ready Pilot",
78+
},
79+
];
80+
1681
let cached: PilotLong[] = [];
1782
let deleted: string[] = [];
1883
let updated: PilotShort[] = [];
@@ -23,7 +88,7 @@ export async function mapPilots(latestVatsimData: VatsimData): Promise<PilotLong
2388
updated = [];
2489
added = [];
2590

26-
const pilotsLong: PilotLong[] = latestVatsimData.pilots.map((pilot) => {
91+
const pilotsLongPromises: Promise<PilotLong>[] = latestVatsimData.pilots.map(async (pilot) => {
2792
const id = `${pilot.cid}_${pilot.callsign}_${pilot.logon_time}`;
2893
const cachedPilot = cached.find((c) => c.id === id);
2994

@@ -59,9 +124,9 @@ export async function mapPilots(latestVatsimData: VatsimData): Promise<PilotLong
59124
aircraft: pilot.flight_plan?.aircraft_short || "A320",
60125
name: pilot.name,
61126
server: pilot.server,
62-
pilot_rating: pilot.pilot_rating,
63-
military_rating: pilot.military_rating,
64-
flight_plan: mapPilotFlightPlan(pilot.flight_plan),
127+
pilot_rating: PILOT_RATINGS.find((r) => r.id === pilot.pilot_rating)?.short_name || "NEW",
128+
military_rating: MILITARY_RATINGS.find((r) => r.id === pilot.military_rating)?.short_name || "M0",
129+
flight_plan: await mapPilotFlightPlan(pilot.flight_plan),
65130
route: `${pilot.flight_plan?.departure || "N/A"} -- ${pilot.flight_plan?.arrival || "N/A"}`,
66131
logon_time: new Date(pilot.logon_time),
67132
times: null,
@@ -76,6 +141,8 @@ export async function mapPilots(latestVatsimData: VatsimData): Promise<PilotLong
76141
return pilotLong;
77142
});
78143

144+
const pilotsLong = await Promise.all(pilotsLongPromises);
145+
79146
// Fetch airport coordinates for flight time estimation and store in PilotLong to minimize DB access
80147
const icaos = getUniqueAirports(pilotsLong);
81148
const airports = (await rdsGetMultiple("static_airport", icaos)) as (StaticAirport | null)[];
@@ -149,11 +216,11 @@ function calculateVerticalSpeed(current: PilotLong, cache: PilotLong | undefined
149216
return Math.round(vs);
150217
}
151218

152-
function mapPilotFlightPlan(fp?: VatsimPilotFlightPlan): PilotFlightPlan | null {
219+
async function mapPilotFlightPlan(fp?: VatsimPilotFlightPlan): Promise<PilotFlightPlan | null> {
153220
if (!fp) return null;
154221
return {
155222
flight_rules: fp.flight_rules === "I" ? "IFR" : "VFR",
156-
ac_reg: extractAircraftRegistration(fp.remarks),
223+
ac_reg: await extractAircraftRegistration(fp.remarks),
157224
departure: { icao: fp.departure },
158225
arrival: { icao: fp.arrival },
159226
alternate: { icao: fp.alternate },
@@ -167,9 +234,27 @@ function mapPilotFlightPlan(fp?: VatsimPilotFlightPlan): PilotFlightPlan | null
167234
};
168235
}
169236

170-
function extractAircraftRegistration(remarks: string): string | null {
237+
async function extractAircraftRegistration(remarks: string): Promise<string | null> {
171238
const match = remarks.match(/REG\/([A-Z0-9]+)/i);
172-
return match?.[1] ?? null;
239+
if (!match?.[1]) return null;
240+
const reg = match[1].toUpperCase();
241+
242+
let aircraft = await rdsGetSingle(`fleet:${reg}`);
243+
if (aircraft) return reg;
244+
245+
if (reg.length > 1) {
246+
const format1 = `${reg[0]}-${reg.slice(1)}`;
247+
aircraft = await rdsGetSingle(`fleet:${format1}`);
248+
if (aircraft) return format1;
249+
}
250+
251+
if (reg.length > 2) {
252+
const format2 = `${reg.slice(0, 2)}-${reg.slice(2)}`;
253+
aircraft = await rdsGetSingle(`fleet:${format2}`);
254+
if (aircraft) return format2;
255+
}
256+
257+
return reg;
173258
}
174259

175260
function mapPilotTimes(current: PilotLong, cache: PilotLong | undefined, vatsimPilot: VatsimPilot): PilotTimes | null {

apps/updater/package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@
2020
"axios": "^1.13.2",
2121
"cron": "^4.3.4",
2222
"csv-parser": "^3.2.0",
23-
"dotenv": "^17.2.3"
23+
"dotenv": "^17.2.3",
24+
"stream-json": "^1.9.1",
25+
"undici": "^7.16.0"
2426
}
2527
}

apps/updater/src/airlines.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { rdsSetSingle } from "@sk/db/redis";
1+
import { rdsGetSingle, rdsSetSingle } from "@sk/db/redis";
22
import axios from "axios";
33

44
const RELEASE_URL = "https://api.github.com/repos/sebastiankrll/simradar24-data/releases/latest";
@@ -7,6 +7,9 @@ const BASE_DATA_URL = "https://github.com/sebastiankrll/simradar24-data/releases
77
let version: string | null = null;
88

99
export async function updateAirlines(): Promise<void> {
10+
if (!version) {
11+
await initVersion();
12+
}
1013
if (!(await isNewRelease())) return;
1114

1215
try {
@@ -17,12 +20,21 @@ export async function updateAirlines(): Promise<void> {
1720
});
1821

1922
await rdsSetSingle("static_airlines:all", response.data);
20-
await rdsSetSingle("static_airlines:version", version?.replace(/^v/, "") || "1.0.0");
23+
await rdsSetSingle("static_airlines:version", version || "1.0.0");
24+
25+
console.log(`✅ Airlines data updated to version ${version}`);
2126
} catch (error) {
2227
console.error(`Error checking for new airlines data: ${error}`);
2328
}
2429
}
2530

31+
async function initVersion(): Promise<void> {
32+
if (!version) {
33+
const redisVersion = await rdsGetSingle("static_airlines:version");
34+
version = redisVersion || "0.0.0";
35+
}
36+
}
37+
2638
async function isNewRelease(): Promise<boolean> {
2739
try {
2840
const response = await axios.get(RELEASE_URL);

apps/updater/src/airports.ts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,19 @@
1-
import { rdsSetMultiple, rdsSetSingle } from "@sk/db/redis";
1+
import { rdsGetSingle, rdsSetMultiple, rdsSetSingle } from "@sk/db/redis";
22
import type { OurAirportsCsv, StaticAirport } from "@sk/types/db";
33
import axios from "axios";
44
import csvParser from "csv-parser";
55

66
const CSV_URL = "https://ourairports.com/data/airports.csv";
77

8+
let version: string | null = null;
9+
810
export async function updateAirports(): Promise<void> {
11+
if (!version) {
12+
await initVersion();
13+
}
14+
if (version === "1.0.0") {
15+
return;
16+
}
917
const response = await axios.get(CSV_URL, { responseType: "stream" });
1018
const airports: OurAirportsCsv[] = [];
1119

@@ -32,4 +40,13 @@ export async function updateAirports(): Promise<void> {
3240
await rdsSetMultiple(filteredAirports, "static_airport", (a) => a.id, "airports:static");
3341
await rdsSetSingle("static_airports:all", filteredAirports);
3442
await rdsSetSingle("static_airports:version", "1.0.0");
43+
44+
console.log(`✅ Airports data updated to version ${version}`);
45+
}
46+
47+
async function initVersion(): Promise<void> {
48+
if (!version) {
49+
const redisVersion = await rdsGetSingle("static_airports:version");
50+
version = redisVersion || "0.0.0";
51+
}
3552
}

apps/updater/src/fir.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { rdsSetSingle } from "@sk/db/redis";
1+
import { rdsGetSingle, rdsSetSingle } from "@sk/db/redis";
22
import type { FIRFeature, FIRProperties, VatSpyDat, VatSpyFIRFeatureCollection } from "@sk/types/db";
33
import axios from "axios";
44

@@ -8,6 +8,9 @@ const BASE_DATA_URL = "https://github.com/vatsimnetwork/vatspy-data-project/rele
88
let version: string | null = null;
99

1010
export async function updateFirs(): Promise<void> {
11+
if (!version) {
12+
await initVersion();
13+
}
1114
if (!(await isNewRelease())) return;
1215

1316
try {
@@ -43,12 +46,21 @@ export async function updateFirs(): Promise<void> {
4346
});
4447

4548
await rdsSetSingle("static_firs:all", newFeatures);
46-
await rdsSetSingle("static_firs:version", version?.replace(/^v/, "") || "1.0.0");
49+
await rdsSetSingle("static_firs:version", version || "1.0.0");
50+
51+
console.log(`✅ FIR data updated to version ${version}`);
4752
} catch (error) {
4853
console.error(`Error checking for new FIR data: ${error}`);
4954
}
5055
}
5156

57+
async function initVersion(): Promise<void> {
58+
if (!version) {
59+
const redisVersion = await rdsGetSingle("static_firs:version");
60+
version = redisVersion || "0.0.0";
61+
}
62+
}
63+
5264
async function isNewRelease(): Promise<boolean> {
5365
try {
5466
const response = await axios.get(RELEASE_URL);

apps/updater/src/fleet.ts

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import { pipeline } from "node:stream/promises";
2+
import { rdsGetSingle, rdsSetMultiple, rdsSetSingle } from "@sk/db/redis";
3+
import type { StaticAircraft } from "@sk/types/db";
4+
import axios from "axios";
5+
import { parser } from "stream-json";
6+
import { streamArray } from "stream-json/streamers/StreamArray";
7+
import { fetch } from "undici";
8+
9+
const RELEASE_URL = "https://api.github.com/repos/sebastiankrll/simradar24-data/releases/latest";
10+
const BASE_DATA_URL = "https://github.com/sebastiankrll/simradar24-data/releases/download/";
11+
12+
let version: string | null = null;
13+
14+
export async function updateFleets(): Promise<void> {
15+
if (!version) {
16+
await initVersion();
17+
}
18+
if (!(await isNewRelease())) return;
19+
20+
try {
21+
const fleetsJsonUrl = `${BASE_DATA_URL}${version}/fleets.json`;
22+
const response = await fetch(fleetsJsonUrl);
23+
24+
if (!response.body) {
25+
throw new Error("No response body from GitHub");
26+
}
27+
28+
const itemsBuffer: StaticAircraft[] = [];
29+
const CHUNK = 10_000;
30+
31+
await pipeline(response.body, parser(), streamArray(), async (stream) => {
32+
for await (const { value } of stream) {
33+
itemsBuffer.push(value);
34+
35+
if (itemsBuffer.length >= CHUNK) {
36+
await rdsSetMultiple(itemsBuffer, "fleet", (a) => a.registration);
37+
itemsBuffer.length = 0;
38+
}
39+
}
40+
});
41+
42+
if (itemsBuffer.length > 0) {
43+
await rdsSetMultiple(itemsBuffer, "fleet", (a) => a.registration);
44+
}
45+
46+
await rdsSetSingle("static_fleets:version", version || "1.0.0");
47+
48+
console.log(`✅ Fleets data updated to version ${version}`);
49+
} catch (error) {
50+
console.error(`Error checking for new fleets data: ${error}`);
51+
}
52+
}
53+
54+
async function initVersion(): Promise<void> {
55+
if (!version) {
56+
const redisVersion = await rdsGetSingle("static_fleets:version");
57+
version = redisVersion || "0.0.0";
58+
}
59+
}
60+
61+
async function isNewRelease(): Promise<boolean> {
62+
try {
63+
const response = await axios.get(RELEASE_URL);
64+
const release = response.data.tag_name;
65+
66+
if (release !== version) {
67+
version = release;
68+
return true;
69+
}
70+
} catch (error) {
71+
console.error(`Error checking for updates: ${error}`);
72+
}
73+
74+
return false;
75+
}

0 commit comments

Comments
 (0)