13 Commits

Author SHA1 Message Date
Nicolas
4f22d48e83 Merge pull request #99 from VAR-Virtual-Air-Rescue/staging
Release v2.0.1
2025-07-25 02:25:07 +02:00
Nicolas
e9c1cf0c94 Merge pull request #84 from VAR-Virtual-Air-Rescue/staging
Namenseinstellungen und Markdown Bug Fix
2025-07-23 12:14:33 +02:00
Nicolas
940d62fdd5 Merge pull request #83 from VAR-Virtual-Air-Rescue/staging
make V1 login not case sensitive
2025-07-23 10:33:21 +02:00
PxlLoewe
644fee3e29 Merge pull request #81 from VAR-Virtual-Air-Rescue/staging
fixed condition for discord role assignment
2025-07-22 21:40:07 -07:00
PxlLoewe
d2a865c955 Merge pull request #80 from VAR-Virtual-Air-Rescue/staging
typos
2025-07-22 19:38:40 -07:00
PxlLoewe
33ec5574f2 Merge pull request #79 from VAR-Virtual-Air-Rescue/staging
added more delay to moodle ID lookup
2025-07-22 13:19:17 -07:00
PxlLoewe
8c6057fe6a Merge pull request #78 from VAR-Virtual-Air-Rescue/staging
Bug Fixes
2025-07-22 12:16:10 -07:00
PxlLoewe
25769f551a Merge pull request #77 from VAR-Virtual-Air-Rescue/staging
Final Bugfixes
2025-07-22 11:16:44 -07:00
PxlLoewe
a5998fbe0f Merge pull request #75 from VAR-Virtual-Air-Rescue/staging
Nutzerliste aus V1 übernommen
2025-07-22 09:12:47 -07:00
PxlLoewe
92e550736b Merge pull request #74 from VAR-Virtual-Air-Rescue/staging
release V2.0
2025-07-22 09:05:14 -07:00
PxlLoewe
616d3d3a61 Merge pull request #70 from VAR-Virtual-Air-Rescue/staging
Fix prod-workflow
2025-07-18 13:40:07 -07:00
PxlLoewe
df7f1b8cd1 Merge pull request #69 from VAR-Virtual-Air-Rescue/staging
Datentypen in Prod DB vorbereiten für release
2025-07-18 13:05:13 -07:00
PxlLoewe
eb98971e8a Merge pull request #47 from VAR-Virtual-Air-Rescue/staging
CD Deployment
2025-07-08 23:23:24 -07:00
19 changed files with 176 additions and 212 deletions

View File

@@ -14,118 +14,113 @@ export const sendAlert = async (
connectedAircrafts: ConnectedAircraft[]; connectedAircrafts: ConnectedAircraft[];
mission: Mission; mission: Mission;
}> => { }> => {
try { const mission = await prisma.mission.findUnique({
const mission = await prisma.mission.findUnique({ where: { id: id },
where: { id: id }, });
}); const Stations = await prisma.station.findMany({
const Stations = await prisma.station.findMany({ where: {
where: { id: {
id: { in: mission?.missionStationIds,
in: mission?.missionStationIds,
},
}, },
},
});
if (!mission) {
throw new Error("Mission not found");
}
// connectedAircrafts the alert is sent to
const connectedAircrafts = await prisma.connectedAircraft.findMany({
where: {
stationId: stationId
? stationId
: {
in: mission.missionStationIds,
},
logoutTime: null,
},
include: {
Station: true,
},
});
for (const aircraft of connectedAircrafts) {
io.to(`station:${aircraft.stationId}`).emit("mission-alert", {
...mission,
Stations,
});
io.to(`desktop:${aircraft.userId}`).emit("mission-alert", {
missionId: mission.id,
}); });
if (!mission) { const user = await prisma.user.findUnique({
throw new Error("Mission not found"); where: { id: aircraft.userId },
});
if (!user) continue;
if (user.settingsNtfyRoom) {
await sendNtfyMission(mission, Stations, aircraft.Station, user.settingsNtfyRoom);
} }
const existingMissionOnStationUser = await prisma.missionOnStationUsers.findFirst({
// connectedAircrafts the alert is sent to
const connectedAircrafts = await prisma.connectedAircraft.findMany({
where: { where: {
stationId: stationId missionId: mission.id,
? stationId userId: aircraft.userId,
: { stationId: aircraft.stationId,
in: mission.missionStationIds,
},
logoutTime: null,
},
include: {
Station: true,
}, },
}); });
for (const aircraft of connectedAircrafts) { if (!existingMissionOnStationUser)
io.to(`station:${aircraft.stationId}`).emit("mission-alert", { await prisma.missionOnStationUsers.create({
...mission, data: {
Stations,
});
io.to(`desktop:${aircraft.userId}`).emit("mission-alert", {
missionId: mission.id,
});
const user = await prisma.user.findUnique({
where: { id: aircraft.userId },
});
if (!user) continue;
if (user.settingsNtfyRoom) {
await sendNtfyMission(mission, Stations, aircraft.Station, user.settingsNtfyRoom);
}
const existingMissionOnStationUser = await prisma.missionOnStationUsers.findFirst({
where: {
missionId: mission.id, missionId: mission.id,
userId: aircraft.userId, userId: aircraft.userId,
stationId: aircraft.stationId, stationId: aircraft.stationId,
}, },
}); });
if (!existingMissionOnStationUser)
await prisma.missionOnStationUsers.create({
data: {
missionId: mission.id,
userId: aircraft.userId,
stationId: aircraft.stationId,
},
});
}
// for statistics only
await prisma.missionsOnStations
.createMany({
data: mission.missionStationIds.map((stationId) => ({
missionId: mission.id,
stationId,
})),
})
.catch((err) => {
// Ignore if the entry already exists
});
if (user === "HPG") {
await prisma.mission.update({
where: { id: Number(id) },
data: {
state: "running",
missionLog: {
push: {
type: "alert-log",
auto: true,
timeStamp: new Date().toISOString(),
} as any,
},
},
});
} else {
await prisma.mission.update({
where: { id: Number(id) },
data: {
state: "running",
missionLog: {
push: {
type: "alert-log",
auto: false,
timeStamp: new Date().toISOString(),
data: {
stationId: stationId,
user: getPublicUser(user, { ignorePrivacy: true }),
},
} as any,
},
},
});
}
return { connectedAircrafts, mission };
} catch (error) {
console.error("Error sending mission alert:", error);
throw new Error("Ein Fehler ist aufgetreten. Bitte melde den Fehler als Bug");
} }
// for statistics only
await prisma.missionsOnStations
.createMany({
data: mission.missionStationIds.map((stationId) => ({
missionId: mission.id,
stationId,
})),
})
.catch((err) => {
// Ignore if the entry already exists
});
if (user === "HPG") {
await prisma.mission.update({
where: { id: Number(id) },
data: {
state: "running",
missionLog: {
push: {
type: "alert-log",
auto: true,
timeStamp: new Date().toISOString(),
} as any,
},
},
});
} else {
await prisma.mission.update({
where: { id: Number(id) },
data: {
state: "running",
missionLog: {
push: {
type: "alert-log",
auto: false,
timeStamp: new Date().toISOString(),
data: {
stationId: stationId,
user: getPublicUser(user, { ignorePrivacy: true }),
},
} as any,
},
},
});
}
return { connectedAircrafts, mission };
}; };

View File

@@ -50,7 +50,10 @@ const getRthCallsigns = (mission: Mission, stations: Station[]) => {
return `🚁 RTH${callsigns.length > 1 ? "s" : ""}: ${callsigns.join(" / ")} `; return `🚁 RTH${callsigns.length > 1 ? "s" : ""}: ${callsigns.join(" / ")} `;
}; };
const getNtfyHeader = (mission: Mission, clientStation: Station): NtfyHeader => ({ const getNtfyHeader = (
mission: Mission,
clientStation: Station,
): NtfyHeader => ({
headers: { headers: {
Title: `${clientStation.bosCallsignShort} / ${mission.missionKeywordAbbreviation} / ${mission.missionKeywordCategory}`, Title: `${clientStation.bosCallsignShort} / ${mission.missionKeywordAbbreviation} / ${mission.missionKeywordCategory}`,
Tags: "pager", Tags: "pager",
@@ -73,13 +76,9 @@ export const sendNtfyMission = async (
clientStation: Station, clientStation: Station,
ntfyRoom: string, ntfyRoom: string,
) => { ) => {
try { axios.post(
await axios.post( `https://ntfy.sh/${ntfyRoom}`,
`https://ntfy.sh/${ntfyRoom}`, getNtfyData(mission, stations),
getNtfyData(mission, stations), getNtfyHeader(mission, clientStation),
getNtfyHeader(mission, clientStation), );
);
} catch (error) {
console.error("Error sending Ntfy mission:", error);
}
}; };

View File

@@ -34,7 +34,7 @@ router.patch("/:id", async (req, res) => {
}, },
}); });
if (discordAccount?.id && !disaptcherUpdate.ghostMode) { if (discordAccount?.id) {
await renameMember( await renameMember(
discordAccount.discordId.toString(), discordAccount.discordId.toString(),
`${getPublicUser(newDispatcher.user).fullName}${newDispatcher.zone}`, `${getPublicUser(newDispatcher.user).fullName}${newDispatcher.zone}`,

View File

@@ -189,11 +189,7 @@ router.post("/:id/send-alert", async (req, res) => {
return; return;
} catch (error) { } catch (error) {
console.error(error); console.error(error);
res res.status(500).json({ error: "Failed to send mission" });
.status(500)
.json({
error: `Ein Fehler ist aufgetreten. Bitte melde den Fehler als Bug (${(error as Error).message})`,
});
return; return;
} }
}); });

View File

@@ -70,7 +70,7 @@ export const handleConnectDispatch =
userId: user.id, userId: user.id,
}, },
}); });
if (discordAccount?.id && !ghostMode) { if (discordAccount?.id) {
await renameMember( await renameMember(
discordAccount.discordId.toString(), discordAccount.discordId.toString(),
`${getPublicUser(user).fullName}${selectedZone}`, `${getPublicUser(user).fullName}${selectedZone}`,

View File

@@ -253,7 +253,7 @@ const MissionMarker = ({
tab: "home", tab: "home",
}, },
], ],
close: [], close: openMissionMarker?.map((m) => m.id) || [],
}); });
} }
}; };

View File

@@ -158,7 +158,7 @@ const PopupContent = ({
</span> </span>
<span> <span>
{aircraft.Station.bosCallsign.length > 15 {aircraft.Station.bosCallsign.length > 15
? aircraft.Station.bosCallsignShort ? aircraft.Station.locationStateShort
: aircraft.Station.bosCallsign} : aircraft.Station.bosCallsign}
</span> </span>
</div> </div>

View File

@@ -177,7 +177,6 @@ export const useAudioStore = create<TalkState>((set, get) => ({
if (dispatchState.status === "connected" && dispatchState.connectedDispatcher?.id) { if (dispatchState.status === "connected" && dispatchState.connectedDispatcher?.id) {
changeDispatcherAPI(dispatchState.connectedDispatcher?.id, { changeDispatcherAPI(dispatchState.connectedDispatcher?.id, {
zone: roomName, zone: roomName,
ghostMode: dispatchState.ghostMode,
}); });
} }

View File

@@ -7,59 +7,55 @@ export const handleParticipantFinished = async (
participant: Participant, participant: Participant,
user: User, user: User,
) => { ) => {
try { const discordAccount = await prisma.discordAccount.findFirst({
const discordAccount = await prisma.discordAccount.findFirst({ where: {
where: { userId: user.id,
userId: user.id, },
}, });
});
const badgedToAdd = event.finishedBadges.filter((badge) => { const badgedToAdd = event.finishedBadges.filter((badge) => {
return !user.badges.includes(badge); return !user.badges.includes(badge);
}); });
const permissionsToAdd = event.finishedPermissions.filter((permission) => { const permissionsToAdd = event.finishedPermissions.filter((permission) => {
return !user.permissions.includes(permission); return !user.permissions.includes(permission);
}); });
await prisma.user.update({ await prisma.user.update({
where: { where: {
id: user.id, id: user.id,
},
data: {
badges: {
push: badgedToAdd,
}, },
data: { permissions: {
badges: { push: permissionsToAdd,
push: badgedToAdd,
},
permissions: {
push: permissionsToAdd,
},
}, },
}); },
});
if (discordAccount) { if (discordAccount) {
await setStandardName({ await setStandardName({
memberId: discordAccount.discordId, memberId: discordAccount.discordId,
userId: user.id, userId: user.id,
});
}
await sendCourseCompletedEmail(user.email, user, event);
await prisma.participant.update({
where: {
id: participant.id,
},
data: {
statusLog: {
push: {
event: "Berechtigungen und Badges vergeben",
timestamp: new Date(),
user: "system",
},
},
},
}); });
} catch (error) {
console.error("Error handling participant finished:", error);
} }
await sendCourseCompletedEmail(user.email, user, event);
await prisma.participant.update({
where: {
id: participant.id,
},
data: {
statusLog: {
push: {
event: "Berechtigungen und Badges vergeben",
timestamp: new Date(),
user: "system",
},
},
},
});
}; };
export const handleParticipantEnrolled = async ( export const handleParticipantEnrolled = async (

View File

@@ -34,61 +34,43 @@ const initTransporter = () => {
initTransporter(); initTransporter();
export const sendCourseCompletedEmail = async (to: string, user: User, event: Event) => { export const sendCourseCompletedEmail = async (to: string, user: User, event: Event) => {
try { const emailHtml = await renderCourseCompleted({ user, event });
const emailHtml = await renderCourseCompleted({ user, event });
if (!transporter) { if (!transporter) {
console.error("Transporter is not initialized"); console.error("Transporter is not initialized");
return; return;
}
await sendMail(to, `Kurs ${event.name} erfolgreich abgeschlossen`, emailHtml);
} catch (error) {
console.error("Error sending course completed email:", error);
} }
sendMail(to, `Kurs ${event.name} erfolgreich abgeschlossen`, emailHtml);
}; };
export const sendPasswordChanged = async (to: string, user: User, password: string) => { export const sendPasswordChanged = async (to: string, user: User, password: string) => {
try { const emailHtml = await renderPasswordChanged({ user, password });
const emailHtml = await renderPasswordChanged({ user, password });
await sendMail(to, `Dein Passwort wurde geändert`, emailHtml); await sendMail(to, `Dein Passwort wurde geändert`, emailHtml);
} catch (error) {}
}; };
export const sendEmailVerification = async (to: string, user: User, code: string) => { export const sendEmailVerification = async (to: string, user: User, code: string) => {
try { const emailHtml = await renderVerificationCode({
const emailHtml = await renderVerificationCode({ user,
user, code,
code, });
}); await sendMail(to, "Bestätige deine E-Mail-Adresse", emailHtml);
await sendMail(to, "Bestätige deine E-Mail-Adresse", emailHtml);
} catch (error) {
console.error("Error sending email verification:", error);
}
}; };
export const sendBannEmail = async (to: string, user: User, staffName: string) => { export const sendBannEmail = async (to: string, user: User, staffName: string) => {
try { const emailHtml = await renderBannNotice({
const emailHtml = await renderBannNotice({ user,
user, staffName,
staffName, });
}); await sendMail(to, "Deine Sperrung bei Virtual Air Rescue", emailHtml);
await sendMail(to, "Deine Sperrung bei Virtual Air Rescue", emailHtml);
} catch (error) {
console.error("Error sending ban email:", error);
}
}; };
export const sendTimebannEmail = async (to: string, user: User, staffName: string) => { export const sendTimebannEmail = async (to: string, user: User, staffName: string) => {
try { const emailHtml = await renderTimeBanNotice({
const emailHtml = await renderTimeBanNotice({ user,
user, staffName,
staffName, });
}); await sendMail(to, "Deine vorrübergehende Sperrung bei Virtual Air Rescue", emailHtml);
await sendMail(to, "Deine vorrübergehende Sperrung bei Virtual Air Rescue", emailHtml);
} catch (error) {
console.error("Error sending time ban email:", error);
}
}; };
export const sendMail = async (to: string, subject: string, html: string) => export const sendMail = async (to: string, subject: string, html: string) =>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 71 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 88 KiB

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 100 KiB

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 129 KiB

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

After

Width:  |  Height:  |  Size: 552 KiB

View File

@@ -22,16 +22,13 @@ export const getPublicUser = (
}, },
): PublicUser => { ): PublicUser => {
const lastName = user.lastname const lastName = user.lastname
.trim()
.split(" ") .split(" ")
.map((part) => `${part[0] || ""}.`) .map((part) => `${part[0]}.`)
.join(" "); .join(" ");
return { return {
firstname: user.firstname, firstname: user.firstname,
lastname: user.settingsHideLastname && !options.ignorePrivacy ? "" : lastName.trim(), // Only take the first letter of each section of the last name lastname: user.settingsHideLastname && !options.ignorePrivacy ? "" : lastName, // Only take the first letter of each section of the last name
fullName: fullName: `${user.firstname} ${user.settingsHideLastname && !options.ignorePrivacy ? "" : lastName}`,
`${user.firstname} ${user.settingsHideLastname && !options.ignorePrivacy ? "" : lastName}`.trim(),
publicId: user.publicId, publicId: user.publicId,
badges: user.badges, badges: user.badges,
}; };