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[];
mission: Mission;
}> => {
try {
const mission = await prisma.mission.findUnique({
where: { id: id },
});
const Stations = await prisma.station.findMany({
where: {
id: {
in: mission?.missionStationIds,
},
const mission = await prisma.mission.findUnique({
where: { id: id },
});
const Stations = await prisma.station.findMany({
where: {
id: {
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) {
throw new Error("Mission not found");
const user = await prisma.user.findUnique({
where: { id: aircraft.userId },
});
if (!user) continue;
if (user.settingsNtfyRoom) {
await sendNtfyMission(mission, Stations, aircraft.Station, user.settingsNtfyRoom);
}
// connectedAircrafts the alert is sent to
const connectedAircrafts = await prisma.connectedAircraft.findMany({
const existingMissionOnStationUser = await prisma.missionOnStationUsers.findFirst({
where: {
stationId: stationId
? stationId
: {
in: mission.missionStationIds,
},
logoutTime: null,
},
include: {
Station: true,
missionId: mission.id,
userId: aircraft.userId,
stationId: aircraft.stationId,
},
});
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,
});
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: {
if (!existingMissionOnStationUser)
await prisma.missionOnStationUsers.create({
data: {
missionId: mission.id,
userId: aircraft.userId,
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(" / ")} `;
};
const getNtfyHeader = (mission: Mission, clientStation: Station): NtfyHeader => ({
const getNtfyHeader = (
mission: Mission,
clientStation: Station,
): NtfyHeader => ({
headers: {
Title: `${clientStation.bosCallsignShort} / ${mission.missionKeywordAbbreviation} / ${mission.missionKeywordCategory}`,
Tags: "pager",
@@ -73,13 +76,9 @@ export const sendNtfyMission = async (
clientStation: Station,
ntfyRoom: string,
) => {
try {
await axios.post(
`https://ntfy.sh/${ntfyRoom}`,
getNtfyData(mission, stations),
getNtfyHeader(mission, clientStation),
);
} catch (error) {
console.error("Error sending Ntfy mission:", error);
}
axios.post(
`https://ntfy.sh/${ntfyRoom}`,
getNtfyData(mission, stations),
getNtfyHeader(mission, clientStation),
);
};

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -7,59 +7,55 @@ export const handleParticipantFinished = async (
participant: Participant,
user: User,
) => {
try {
const discordAccount = await prisma.discordAccount.findFirst({
where: {
userId: user.id,
},
});
const discordAccount = await prisma.discordAccount.findFirst({
where: {
userId: user.id,
},
});
const badgedToAdd = event.finishedBadges.filter((badge) => {
return !user.badges.includes(badge);
});
const permissionsToAdd = event.finishedPermissions.filter((permission) => {
return !user.permissions.includes(permission);
});
const badgedToAdd = event.finishedBadges.filter((badge) => {
return !user.badges.includes(badge);
});
const permissionsToAdd = event.finishedPermissions.filter((permission) => {
return !user.permissions.includes(permission);
});
await prisma.user.update({
where: {
id: user.id,
await prisma.user.update({
where: {
id: user.id,
},
data: {
badges: {
push: badgedToAdd,
},
data: {
badges: {
push: badgedToAdd,
},
permissions: {
push: permissionsToAdd,
},
permissions: {
push: permissionsToAdd,
},
});
},
});
if (discordAccount) {
await setStandardName({
memberId: discordAccount.discordId,
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",
},
},
},
if (discordAccount) {
await setStandardName({
memberId: discordAccount.discordId,
userId: user.id,
});
} 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 (

View File

@@ -34,61 +34,43 @@ const initTransporter = () => {
initTransporter();
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) {
console.error("Transporter is not initialized");
return;
}
await sendMail(to, `Kurs ${event.name} erfolgreich abgeschlossen`, emailHtml);
} catch (error) {
console.error("Error sending course completed email:", error);
if (!transporter) {
console.error("Transporter is not initialized");
return;
}
sendMail(to, `Kurs ${event.name} erfolgreich abgeschlossen`, emailHtml);
};
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);
} catch (error) {}
await sendMail(to, `Dein Passwort wurde geändert`, emailHtml);
};
export const sendEmailVerification = async (to: string, user: User, code: string) => {
try {
const emailHtml = await renderVerificationCode({
user,
code,
});
await sendMail(to, "Bestätige deine E-Mail-Adresse", emailHtml);
} catch (error) {
console.error("Error sending email verification:", error);
}
const emailHtml = await renderVerificationCode({
user,
code,
});
await sendMail(to, "Bestätige deine E-Mail-Adresse", emailHtml);
};
export const sendBannEmail = async (to: string, user: User, staffName: string) => {
try {
const emailHtml = await renderBannNotice({
user,
staffName,
});
await sendMail(to, "Deine Sperrung bei Virtual Air Rescue", emailHtml);
} catch (error) {
console.error("Error sending ban email:", error);
}
const emailHtml = await renderBannNotice({
user,
staffName,
});
await sendMail(to, "Deine Sperrung bei Virtual Air Rescue", emailHtml);
};
export const sendTimebannEmail = async (to: string, user: User, staffName: string) => {
try {
const emailHtml = await renderTimeBanNotice({
user,
staffName,
});
await sendMail(to, "Deine vorrübergehende Sperrung bei Virtual Air Rescue", emailHtml);
} catch (error) {
console.error("Error sending time ban email:", error);
}
const emailHtml = await renderTimeBanNotice({
user,
staffName,
});
await sendMail(to, "Deine vorrübergehende Sperrung bei Virtual Air Rescue", emailHtml);
};
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 => {
const lastName = user.lastname
.trim()
.split(" ")
.map((part) => `${part[0] || ""}.`)
.map((part) => `${part[0]}.`)
.join(" ");
return {
firstname: user.firstname,
lastname: user.settingsHideLastname && !options.ignorePrivacy ? "" : lastName.trim(), // Only take the first letter of each section of the last name
fullName:
`${user.firstname} ${user.settingsHideLastname && !options.ignorePrivacy ? "" : lastName}`.trim(),
lastname: user.settingsHideLastname && !options.ignorePrivacy ? "" : lastName, // Only take the first letter of each section of the last name
fullName: `${user.firstname} ${user.settingsHideLastname && !options.ignorePrivacy ? "" : lastName}`,
publicId: user.publicId,
badges: user.badges,
};