From 060810f1b0622be9fd24b030916ea583a32f7307 Mon Sep 17 00:00:00 2001 From: PxlLoewe <72106766+PxlLoewe@users.noreply.github.com> Date: Mon, 19 May 2025 23:11:33 -0700 Subject: [PATCH] added ntfy --- apps/dispatch-server/modules/ntfy.ts | 84 ++++++++++++++++++ apps/dispatch-server/routes/mission.ts | 16 ++++ .../app/_components/QueryProvider.tsx | 1 + .../app/_store/pilot/connectionStore.ts | 11 ++- apps/dispatch/app/_store/pilot/dmeStore.ts | 66 ++++++++------ .../app/pilot/_components/dme/useButtons.ts | 8 +- .../pilot/_components/navbar/Connection.tsx | 22 +++-- .../app/(app)/settings/_components/forms.tsx | 68 +++++++++++++- apps/hub/app/(app)/settings/page.tsx | 76 +++++++++------- grafana/grafana.db | Bin 1122304 -> 1122304 bytes packages/database/prisma/schema/user.prisma | 1 + 11 files changed, 277 insertions(+), 76 deletions(-) create mode 100644 apps/dispatch-server/modules/ntfy.ts diff --git a/apps/dispatch-server/modules/ntfy.ts b/apps/dispatch-server/modules/ntfy.ts new file mode 100644 index 00000000..bc86df56 --- /dev/null +++ b/apps/dispatch-server/modules/ntfy.ts @@ -0,0 +1,84 @@ +import { Mission, Station } from "@repo/db"; +import axios from "axios"; + +interface NtfyHeader { + headers: { + Title: string; + Tags: string; + Priority: string; + }; +} + +const getLocation = (mission: Mission) => { + return `🏡 Ort: + ${mission.addressStreet}, ${mission.addressZip} ${mission.addressCity}\n\n`; +}; + +const getCordinates = (mission: Mission) => { + let cords = `📍 Koordinaten: + - Breite: ${mission.addressLat} + - Länge: ${mission.addressLng}`; + + return `${cords}\n\n`; +}; + +const getInfo = (mission: Mission) => { + return `⚕️ Info: + ${mission.missionKeywordAbbreviation}\n\n`; +}; + +const getPatientInfo = (mission: Mission) => { + if (!mission.missionPatientInfo) return ""; + + return `🧍‍♂️ Patient: + ${mission.missionPatientInfo}\n\n`; +}; + +const getAdditionalInfo = (mission: Mission) => { + if (!mission.missionAdditionalInfo) return ""; + + return `📋 Anmerkung: + ${mission.missionAdditionalInfo}\n\n`; +}; + +const getRthCallsigns = (mission: Mission, stations: Station[]) => { + const callsigns: Array = []; + stations.forEach((station) => { + callsigns.push(station.bosCallsignShort); + }); + + return `🚁 RTH${callsigns.length > 1 ? "s" : ""}: ${callsigns.join(" / ")} `; +}; + +const getNtfyHeader = ( + mission: Mission, + clientStation: Station, +): NtfyHeader => ({ + headers: { + Title: `${clientStation.bosCallsignShort} / ${mission.missionKeywordAbbreviation} / ${mission.missionKeywordCategory}`, + Tags: "pager", + Priority: "4", + }, +}); + +const getNtfyData = (mission: Mission, stations: Station[]): string => + `${new Date().toLocaleString()}\n\n` + + `${getInfo(mission)}` + + `${getLocation(mission)}` + + `${getCordinates(mission)}` + + `${getPatientInfo(mission)}` + + `${getAdditionalInfo(mission)}` + + `${getRthCallsigns(mission, stations)}`; + +export const sendNtfyMission = async ( + mission: Mission, + stations: Station[], + clientStation: Station, + ntfyRoom: string, +) => { + axios.post( + `https://ntfy.sh/${ntfyRoom}`, + getNtfyData(mission, stations), + getNtfyHeader(mission, clientStation), + ); +}; diff --git a/apps/dispatch-server/routes/mission.ts b/apps/dispatch-server/routes/mission.ts index e2251feb..d5492885 100644 --- a/apps/dispatch-server/routes/mission.ts +++ b/apps/dispatch-server/routes/mission.ts @@ -1,6 +1,7 @@ import { prisma } from "@repo/db"; import { Router } from "express"; import { io } from "../index"; +import { sendNtfyMission } from "modules/ntfy"; const router = Router(); @@ -107,6 +108,9 @@ router.post("/:id/send-alert", async (req, res) => { }, logoutTime: null, }, + include: { + Station: true, + }, }); for (const aircraft of connectedAircrafts) { @@ -115,6 +119,18 @@ router.post("/:id/send-alert", async (req, res) => { ...mission, Stations, }); + 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: { diff --git a/apps/dispatch/app/_components/QueryProvider.tsx b/apps/dispatch/app/_components/QueryProvider.tsx index 6e4c9a21..5a9376fb 100644 --- a/apps/dispatch/app/_components/QueryProvider.tsx +++ b/apps/dispatch/app/_components/QueryProvider.tsx @@ -38,6 +38,7 @@ export function QueryProvider({ children }: { children: ReactNode }) { }; const invalidateConenctedAircrafts = () => { + console.log("invalidateConenctedAircrafts"); queryClient.invalidateQueries({ queryKey: ["aircrafts"], }); diff --git a/apps/dispatch/app/_store/pilot/connectionStore.ts b/apps/dispatch/app/_store/pilot/connectionStore.ts index 67b69dc2..4e8e49d5 100644 --- a/apps/dispatch/app/_store/pilot/connectionStore.ts +++ b/apps/dispatch/app/_store/pilot/connectionStore.ts @@ -1,6 +1,6 @@ import { create } from "zustand"; import { dispatchSocket } from "../../dispatch/socket"; -import { ConnectedAircraft, Mission, Station } from "@repo/db"; +import { ConnectedAircraft, Mission, Station, User } from "@repo/db"; import { pilotSocket } from "pilot/socket"; import { useDmeStore } from "_store/pilot/dmeStore"; @@ -19,6 +19,7 @@ interface ConnectionStore { stationId: string, logoffTime: string, station: Station, + user: User, ) => Promise; disconnect: () => void; } @@ -29,12 +30,18 @@ export const usePilotConnectionStore = create((set) => ({ selectedStation: null, connectedAircraft: null, activeMission: null, - connect: async (uid, stationId, logoffTime, station) => + connect: async (uid, stationId, logoffTime, station, user) => new Promise((resolve) => { set({ status: "connecting", message: "", selectedStation: station }); + pilotSocket.auth = { uid }; pilotSocket.connect(); pilotSocket.once("connect", () => { + useDmeStore.getState().setPage({ + page: "home", + station, + user, + }); pilotSocket.emit("connect-pilot", { logoffTime, stationId, diff --git a/apps/dispatch/app/_store/pilot/dmeStore.ts b/apps/dispatch/app/_store/pilot/dmeStore.ts index e7a20bcb..fc5a2862 100644 --- a/apps/dispatch/app/_store/pilot/dmeStore.ts +++ b/apps/dispatch/app/_store/pilot/dmeStore.ts @@ -43,6 +43,8 @@ interface MrtStore { setLines: (lines: MrtStore["lines"]) => void; } +let interval: NodeJS.Timeout | null = null; + export const useDmeStore = create( syncTabs( (set) => ({ @@ -52,42 +54,52 @@ export const useDmeStore = create( }, lines: [ { - textLeft: "VAR.#", + textLeft: "", + }, + { + textMid: "VAR . DME# No Data", textSize: "2", }, { - textLeft: "No Data", + textLeft: "", }, ], setLines: (lines) => set({ lines }), setPage: (pageData) => { + if (interval) clearInterval(interval); switch (pageData.page) { case "home": { - set({ - page: "home", - lines: [ - { textMid: "⠀" }, - { - textMid: pageData.station.bosCallsign - ? `VAR#.${pageData.station.bosCallsign}` - : "no Data", - style: { fontWeight: "bold" }, - }, - { textMid: "⠀" }, - { - textMid: new Date().toLocaleDateString(), - }, - { - textMid: new Date().toLocaleTimeString(), - style: { fontWeight: "bold" }, - }, - { textMid: "⠀" }, - { - textMid: `${pageData.user.lastname} ${pageData.user.firstname}`, - }, - { textMid: "⠀" }, - ], - }); + const setHomePage = () => + set({ + page: "home", + lines: [ + { textMid: "⠀" }, + { + textMid: pageData.station.bosCallsign + ? `VAR#.${pageData.station.bosCallsign}` + : "no Data", + style: { fontWeight: "bold" }, + }, + { textMid: "⠀" }, + { + textMid: new Date().toLocaleDateString(), + }, + { + textMid: new Date().toLocaleTimeString(), + style: { fontWeight: "bold" }, + }, + { textMid: "⠀" }, + { + textMid: `${pageData.user.lastname} ${pageData.user.firstname}`, + }, + { textMid: "⠀" }, + ], + }); + setHomePage(); + + interval = setInterval(() => { + setHomePage(); + }, 1000); break; } diff --git a/apps/dispatch/app/pilot/_components/dme/useButtons.ts b/apps/dispatch/app/pilot/_components/dme/useButtons.ts index b3064c4e..95d780bb 100644 --- a/apps/dispatch/app/pilot/_components/dme/useButtons.ts +++ b/apps/dispatch/app/pilot/_components/dme/useButtons.ts @@ -10,12 +10,16 @@ export const useButtons = () => { const handleButton = (button: "main" | "menu" | "left" | "right") => () => { switch (button) { case "main": - if (page === "mission") { + if (page === "mission" || page === "new-mission") { setPage({ page: "acknowledge" }); } break; case "menu": - console.log("home", page, { station, user }); + if (page === "mission" || page === "new-mission") { + setPage({ page: "acknowledge" }); + if (station && user) setPage({ page: "home", station, user }); + break; + } if (station && user) { setPage({ page: "home", station, user }); } else { diff --git a/apps/dispatch/app/pilot/_components/navbar/Connection.tsx b/apps/dispatch/app/pilot/_components/navbar/Connection.tsx index 1765dcbb..f911917a 100644 --- a/apps/dispatch/app/pilot/_components/navbar/Connection.tsx +++ b/apps/dispatch/app/pilot/_components/navbar/Connection.tsx @@ -130,16 +130,20 @@ export const ConnectionBtn = () => { type="submit" onSubmit={() => false} onClick={() => { - connection.connect( - uid, - form.selectedStationId?.toString() || "", - form.logoffTime || "", - stations?.find( - (station) => - station.id === - parseInt(form.selectedStationId?.toString() || ""), - )!, + const selectedStation = stations?.find( + (station) => + station.id === + parseInt(form.selectedStationId?.toString() || ""), ); + if (selectedStation) { + connection.connect( + uid, + form.selectedStationId?.toString() || "", + form.logoffTime || "", + selectedStation, + session.data!.user, + ); + } }} className="btn btn-soft btn-info" > diff --git a/apps/hub/app/(app)/settings/_components/forms.tsx b/apps/hub/app/(app)/settings/_components/forms.tsx index 778b9d3c..7e2a57bd 100644 --- a/apps/hub/app/(app)/settings/_components/forms.tsx +++ b/apps/hub/app/(app)/settings/_components/forms.tsx @@ -5,8 +5,6 @@ import { useState } from "react"; import { useForm } from "react-hook-form"; import { z } from "zod"; import { unlinkDiscord, updateUser, changePassword } from "../actions"; -import { Toaster, toast } from "react-hot-toast"; -import { useSession } from "next-auth/react"; import { useRouter } from "next/navigation"; import { Button } from "../../../_components/ui/Button"; import { @@ -21,6 +19,9 @@ import { LockOpen2Icon, LockOpen1Icon, } from "@radix-ui/react-icons"; +import toast from "react-hot-toast"; +import { UserSchema } from "@repo/db/zod"; +import { Bell, Plane } from "lucide-react"; export const ProfileForm = ({ user }: { user: User }) => { const schema = z.object({ @@ -242,7 +243,7 @@ export const SocialForm = ({ ); }; -export const PasswordForm = ({ user }: { user: User }) => { +export const PasswordForm = () => { const schema = z.object({ password: z.string().min(2).max(30), newPassword: z.string().min(2).max(30), @@ -345,3 +346,64 @@ export const PasswordForm = ({ user }: { user: User }) => { ); }; + +export const PilotForm = ({ user }: { user: User }) => { + const [isLoading, setIsLoading] = useState(false); + + const form = useForm({ + defaultValues: user, + resolver: zodResolver(UserSchema), + }); + + if (!user) return null; + return ( +
{ + form.handleSubmit(async () => { + setIsLoading(true); + }); + await updateUser(values); + setIsLoading(false); + form.reset(values); + toast.success("Deine Änderungen wurden gespeichert!", { + style: { + background: "var(--color-base-100)", + color: "var(--color-base-content)", + }, + }); + })} + > +

+ Pilot +

+
+ + {form.formState.errors.settingsNtfyRoom && ( +

+ {form.formState.errors.settingsNtfyRoom.message} +

+ )} +
+
+ +
+
+ ); +}; diff --git a/apps/hub/app/(app)/settings/page.tsx b/apps/hub/app/(app)/settings/page.tsx index adc32eb8..c5a7a858 100644 --- a/apps/hub/app/(app)/settings/page.tsx +++ b/apps/hub/app/(app)/settings/page.tsx @@ -1,38 +1,48 @@ import { getServerSession } from "../../api/auth/[...nextauth]/auth"; import { PrismaClient } from "@repo/db"; -import { ProfileForm, SocialForm, PasswordForm } from "./_components/forms"; +import { + ProfileForm, + SocialForm, + PasswordForm, + PilotForm, +} from "./_components/forms"; import { GearIcon } from "@radix-ui/react-icons"; -export default async () => { - const prisma = new PrismaClient(); - const session = await getServerSession(); - if (!session) return null; - const user = await prisma.user.findFirst({ - where: { - id: session.user.id, - }, - include: { - discordAccounts: true, - }, - }); - if (!user) return null; - const discordAccount = user?.discordAccounts[0]; - return ( -
-
-

- Einstellungen -

-
-
- -
-
- -
-
- -
-
- ); +export const page = async () => { + const prisma = new PrismaClient(); + const session = await getServerSession(); + if (!session) return null; + const user = await prisma.user.findFirst({ + where: { + id: session.user.id, + }, + include: { + discordAccounts: true, + }, + }); + if (!user) return null; + const discordAccount = user?.discordAccounts[0]; + return ( +
+
+

+ Einstellungen +

+
+
+ +
+
+ +
+
+ +
+
+ +
+
+ ); }; + +export default page; diff --git a/grafana/grafana.db b/grafana/grafana.db index bb578f1bdc91a27794ae1bd10496a567eada89f3..b32f901d8806baf90b3ceee904df35d255671910 100644 GIT binary patch delta 135 zcmZoT;L>owWr8&0>xnYXjISFLS`!#s6PQ{Pm|GKAS`%1X6WCf4*taHdlT zYrmz>0mPg@%(eZNKKBPv9wUPg12ZcF11l4wsOblNxFy9+b&U)ZU}AdaMwZ5wrWPi~ Z?QCw`K+FTgygowWr8&0(}^<9j87XAS`!#s6PQ{Pm|GKAS`%1X6WCf4*taHdl0mPg@%(eZNKKBPv9z)9z15+zQ3o9ezsOblNxFy9+bqy^QU}Acf7KR4q7Di^K Z?QCw`K+FTgyg