diff --git a/apps/dispatch-server/routes/mission.ts b/apps/dispatch-server/routes/mission.ts index 458eed75..392ba360 100644 --- a/apps/dispatch-server/routes/mission.ts +++ b/apps/dispatch-server/routes/mission.ts @@ -327,9 +327,6 @@ router.post("/:id/validate-hpg", async (req, res) => { res.json({ message: "HPG validierung gestartet", }); - console.log( - `HPG Validation for ${user?.publicId} (${mission?.hpgSelectedMissionString}) started`, - ); io.to(`desktop:${activeAircraftinMission?.userId}`).emit("hpg-validation", { missionId: parseInt(id), userId: req.user?.id, diff --git a/apps/dispatch-server/socket-events/connect-pilot.ts b/apps/dispatch-server/socket-events/connect-pilot.ts index ca327b1e..d37618d4 100644 --- a/apps/dispatch-server/socket-events/connect-pilot.ts +++ b/apps/dispatch-server/socket-events/connect-pilot.ts @@ -55,14 +55,35 @@ export const handleConnectPilot = } } + // Set "now" to 2 hours in the future + const nowPlus2h = new Date(); + nowPlus2h.setHours(nowPlus2h.getHours() + 2); + + // Generate a random position in Germany (approximate bounding box) + function getRandomGermanPosition() { + const minLat = 47.2701; + const maxLat = 55.0581; + const minLng = 5.8663; + const maxLng = 15.0419; + const lat = Math.random() * (maxLat - minLat) + minLat; + const lng = Math.random() * (maxLng - minLng) + minLng; + return { lat, lng }; + } + + const randomPos = + process.env.environment === "development" ? getRandomGermanPosition() : undefined; + const connectedAircraftEntry = await prisma.connectedAircraft.create({ data: { publicUser: getPublicUser(user) as any, esimatedLogoutTime: parsedLogoffDate?.toISOString() || null, - lastHeartbeat: new Date().toISOString(), userId: userId, - loginTime: new Date().toISOString(), + loginTime: nowPlus2h.toISOString(), stationId: parseInt(stationId), + lastHeartbeat: + process.env.environment === "development" ? nowPlus2h.toISOString() : undefined, + posLat: randomPos?.lat, + posLng: randomPos?.lng, }, }); diff --git a/apps/dispatch/app/_components/map/AircraftMarker.tsx b/apps/dispatch/app/_components/map/AircraftMarker.tsx index f2a920a1..996337bf 100644 --- a/apps/dispatch/app/_components/map/AircraftMarker.tsx +++ b/apps/dispatch/app/_components/map/AircraftMarker.tsx @@ -15,7 +15,6 @@ import { ConnectedAircraft, Station } from "@repo/db"; import { useQuery } from "@tanstack/react-query"; import { getConnectedAircraftPositionLogAPI, getConnectedAircraftsAPI } from "_querys/aircrafts"; import { getMissionsAPI } from "_querys/missions"; -import { checkSimulatorConnected } from "_helpers/simulatorConnected"; import { FMS_STATUS_COLORS, FMS_STATUS_TEXT_COLORS } from "_helpers/fmsStatusColors"; const AircraftPopupContent = ({ @@ -384,16 +383,14 @@ export const AircraftLayer = () => { const { data: aircrafts } = useQuery({ queryKey: ["aircrafts"], queryFn: getConnectedAircraftsAPI, - refetchInterval: 10000, + refetchInterval: 10_000, }); return ( <> - {aircrafts - ?.filter((a) => checkSimulatorConnected(a.lastHeartbeat)) - ?.map((aircraft) => { - return ; - })} + {aircrafts?.map((aircraft) => { + return ; + })} ); }; diff --git a/apps/dispatch/app/_components/map/_components/AircraftMarkerTabs.tsx b/apps/dispatch/app/_components/map/_components/AircraftMarkerTabs.tsx index 35bd80d8..fe91bd48 100644 --- a/apps/dispatch/app/_components/map/_components/AircraftMarkerTabs.tsx +++ b/apps/dispatch/app/_components/map/_components/AircraftMarkerTabs.tsx @@ -1,5 +1,5 @@ "use client"; -import React, { useState } from "react"; +import React, { useEffect, useMemo, useState } from "react"; import { FMS_STATUS_COLORS, FMS_STATUS_TEXT_COLORS } from "_helpers/fmsStatusColors"; import { ConnectedAircraft, @@ -13,7 +13,7 @@ import { Station, } from "@repo/db"; import { toast } from "react-hot-toast"; -import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { editConnectedAircraftAPI } from "_querys/aircrafts"; import { useDispatchConnectionStore } from "_store/dispatch/connectionStore"; import { cn } from "_helpers/cn"; @@ -39,7 +39,9 @@ import { TextSearch, } from "lucide-react"; import { useSession } from "next-auth/react"; -import { editMissionAPI, sendSdsMessageAPI } from "_querys/missions"; +import { sendSdsMessageAPI } from "_querys/missions"; +import { getLivekitRooms } from "_querys/livekit"; +import { findLeitstelleForPosition } from "_helpers/findLeitstelleinPoint"; const FMSStatusHistory = ({ aircraft, @@ -216,14 +218,35 @@ const RettungsmittelTab = ({ aircraft: ConnectedAircraft & { Station: Station }; }) => { const station = aircraft.Station; + const { data: livekitRooms } = useQuery({ + queryKey: ["livekit-rooms"], + queryFn: () => getLivekitRooms(), + refetchInterval: 10000, + }); + + const participants = + livekitRooms?.flatMap((room) => + room.participants.map((p) => ({ + ...p, + roomName: room.room.name, + })), + ) || []; + + const livekitUser = participants.find((p) => (p.attributes.userId = aircraft.userId)); + + const lstName = useMemo(() => { + if (!aircraft.posLng || !aircraft.posLat) return; + return findLeitstelleForPosition(aircraft.posLng, aircraft.posLat); + }, [aircraft]); + return (
  • - Aktuelle Rufgruppe: LST_01 + Aktuelle Rufgruppe: {livekitUser?.roomName || "Nicht verbunden"}
  • - Leitstellenbereich: Florian Berlin + Leitstellenbereich: {lstName || station.bosRadioArea}
@@ -348,7 +371,7 @@ const SDSTab = ({ onClick={() => setIsChatOpen(true)} > - Notiz hinzufügen + SDS senden ) : ( @@ -379,6 +402,7 @@ const SDSTab = ({ }, }) .then(() => { + toast.success("SDS-Nachricht gesendet"); setIsChatOpen(false); setNote(""); }); diff --git a/apps/dispatch/app/_components/map/_components/MarkerCluster.tsx b/apps/dispatch/app/_components/map/_components/MarkerCluster.tsx index 18e30a49..aeb70b02 100644 --- a/apps/dispatch/app/_components/map/_components/MarkerCluster.tsx +++ b/apps/dispatch/app/_components/map/_components/MarkerCluster.tsx @@ -6,7 +6,6 @@ import { useMapStore } from "_store/mapStore"; import { FMS_STATUS_COLORS, FMS_STATUS_TEXT_COLORS } from "_helpers/fmsStatusColors"; import { MISSION_STATUS_COLORS, MISSION_STATUS_TEXT_COLORS } from "_components/map/MissionMarkers"; import { cn } from "_helpers/cn"; -import { checkSimulatorConnected } from "_helpers/simulatorConnected"; import { getConnectedAircraftsAPI } from "_querys/aircrafts"; import { getMissionsAPI } from "_querys/missions"; import { useEffect, useMemo, useState } from "react"; @@ -96,41 +95,39 @@ const PopupContent = ({
); })} - {aircrafts - .filter((a) => checkSimulatorConnected(a.lastHeartbeat)) - .map((aircraft) => ( -
( +
{ + setOpenAircraftMarker({ + open: [ + { + id: aircraft.id, + tab: "aircraft", + }, + ], + close: [], + }); + map.setView([aircraft.posLat!, aircraft.posLng!], 12, { + animate: true, + }); + }} + > + { - setOpenAircraftMarker({ - open: [ - { - id: aircraft.id, - tab: "aircraft", - }, - ], - close: [], - }); - map.setView([aircraft.posLat!, aircraft.posLng!], 12, { - animate: true, - }); + color: FMS_STATUS_TEXT_COLORS[aircraft.fmsStatus], }} > - - {aircraft.fmsStatus} - - {aircraft.Station.bosCallsign} -
- ))} + {aircraft.fmsStatus} + + {aircraft.Station.bosCallsign} +
+ ))}
); @@ -141,6 +138,7 @@ export const MarkerCluster = () => { const { data: aircrafts } = useQuery({ queryKey: ["aircrafts"], queryFn: getConnectedAircraftsAPI, + refetchInterval: 10_000, }); const dispatcherConnected = useDispatchConnectionStore((s) => s.status) === "connected"; @@ -178,38 +176,36 @@ export const MarkerCluster = () => { lat: number; lng: number; }[] = []; - aircrafts - ?.filter((a) => checkSimulatorConnected(a.lastHeartbeat)) - .forEach((aircraft) => { - const lat = aircraft.posLat!; - const lng = aircraft.posLng!; + aircrafts?.forEach((aircraft) => { + const lat = aircraft.posLat!; + const lng = aircraft.posLng!; - const existingClusterIndex = newCluster.findIndex( - (c) => Math.abs(c.lat - lat) < 1 && Math.abs(c.lng - lng) < 1, - ); - const existingCluster = newCluster[existingClusterIndex]; - if (existingCluster) { - newCluster = [...newCluster].map((c, i) => { - if (i === existingClusterIndex) { - return { - ...c, - aircrafts: [...c.aircrafts, aircraft], - }; - } - return c; - }); - } else { - newCluster = [ - ...newCluster, - { - aircrafts: [aircraft], - missions: [], - lat, - lng, - }, - ]; - } - }); + const existingClusterIndex = newCluster.findIndex( + (c) => Math.abs(c.lat - lat) < 1 && Math.abs(c.lng - lng) < 1, + ); + const existingCluster = newCluster[existingClusterIndex]; + if (existingCluster) { + newCluster = [...newCluster].map((c, i) => { + if (i === existingClusterIndex) { + return { + ...c, + aircrafts: [...c.aircrafts, aircraft], + }; + } + return c; + }); + } else { + newCluster = [ + ...newCluster, + { + aircrafts: [aircraft], + missions: [], + lat, + lng, + }, + ]; + } + }); filteredMissions?.forEach((mission) => { const lat = mission.addressLat; const lng = mission.addressLng; diff --git a/apps/dispatch/app/_components/navbar/AdminPanel.tsx b/apps/dispatch/app/_components/navbar/AdminPanel.tsx index 62218809..e1444cab 100644 --- a/apps/dispatch/app/_components/navbar/AdminPanel.tsx +++ b/apps/dispatch/app/_components/navbar/AdminPanel.tsx @@ -35,14 +35,14 @@ export default function AdminPanel() { refetchInterval: 10000, }); const { data: livekitRooms } = useQuery({ - queryKey: ["connected-audio-users"], + queryKey: ["livekit-rooms"], queryFn: () => getLivekitRooms(), refetchInterval: 10000, }); const kickLivekitParticipantMutation = useMutation({ mutationFn: kickLivekitParticipant, onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ["connected-audio-users"] }); + queryClient.invalidateQueries({ queryKey: ["livekit-rooms"] }); }, }); const editUSerMutation = useMutation({ @@ -95,8 +95,6 @@ export default function AdminPanel() { return !pilot && !fDispatcher; }); - console.log("Livekit Rooms", livekitRooms); - const modalRef = useRef(null); return ( diff --git a/apps/dispatch/app/_helpers/findLeitstelleinPoint.ts b/apps/dispatch/app/_helpers/findLeitstelleinPoint.ts new file mode 100644 index 00000000..3d047192 --- /dev/null +++ b/apps/dispatch/app/_helpers/findLeitstelleinPoint.ts @@ -0,0 +1,18 @@ +import { point, multiPolygon, booleanPointInPolygon } from "@turf/turf"; +import leitstellenGeoJSON from "../_components/map/_geojson/Leitstellen.json"; // Pfad anpassen + +export function findLeitstelleForPosition(lat: number, lng: number) { + const heliPoint = point([lat, lng]); + + for (const feature of (leitstellenGeoJSON as any).features) { + if (feature.geometry.type === "MultiPolygon") { + const polygon = multiPolygon(feature.geometry.coordinates); + if (booleanPointInPolygon(heliPoint, polygon)) { + console.log("Point is inside polygon:", feature.properties.name); + return feature.properties.name ?? "Unbenannte Leitstelle"; + } + } + } + + return null; // Keine passende Leitstelle gefunden +} diff --git a/apps/dispatch/app/_helpers/simulatorConnected.ts b/apps/dispatch/app/_helpers/simulatorConnected.ts index 35e3a98d..cf38f637 100644 --- a/apps/dispatch/app/_helpers/simulatorConnected.ts +++ b/apps/dispatch/app/_helpers/simulatorConnected.ts @@ -1,2 +1,7 @@ -export const checkSimulatorConnected = (date: Date) => - date && Date.now() - new Date(date).getTime() <= 3000_000; +import { ConnectedAircraft } from "@repo/db"; + +export const checkSimulatorConnected = (a: ConnectedAircraft) => { + if (!a.lastHeartbeat || Date.now() - new Date(a.lastHeartbeat).getTime() > 30_000) return false; // 30 seconds + if (!a.posLat || !a.posLng) return false; + return true; +}; diff --git a/apps/dispatch/app/_querys/aircrafts.ts b/apps/dispatch/app/_querys/aircrafts.ts index bb89a9a7..f6e11767 100644 --- a/apps/dispatch/app/_querys/aircrafts.ts +++ b/apps/dispatch/app/_querys/aircrafts.ts @@ -1,13 +1,14 @@ import { ConnectedAircraft, PositionLog, Prisma, PublicUser, Station } from "@repo/db"; import axios from "axios"; import { serverApi } from "_helpers/axios"; +import { checkSimulatorConnected } from "_helpers/simulatorConnected"; export const getConnectedAircraftsAPI = async () => { const res = await axios.get<(ConnectedAircraft & { Station: Station })[]>("/api/aircrafts"); // return only connected aircrafts if (res.status !== 200) { throw new Error("Failed to fetch stations"); } - return res.data; + return res.data.filter((a) => checkSimulatorConnected(a)); }; export const editConnectedAircraftAPI = async ( diff --git a/apps/dispatch/app/_store/pilot/MrtStore.ts b/apps/dispatch/app/_store/pilot/MrtStore.ts index f8d70138..ea3a9662 100644 --- a/apps/dispatch/app/_store/pilot/MrtStore.ts +++ b/apps/dispatch/app/_store/pilot/MrtStore.ts @@ -122,7 +122,7 @@ export const useMrtStore = create( }, { textLeft: "ILS VAR#", textSize: "3" }, { - textLeft: "new status received", + textLeft: "empfangen", style: { fontWeight: "bold" }, textSize: "4", }, @@ -136,7 +136,7 @@ export const useMrtStore = create( page: "sds", lines: [ { - textLeft: `neue SDS-Nachricht`, + textLeft: `SDS-Nachricht`, style: { fontWeight: "bold" }, textSize: "2", }, diff --git a/apps/dispatch/app/api/livekit-participant/route.ts b/apps/dispatch/app/api/livekit-participant/route.ts index e337f025..2cf644ee 100644 --- a/apps/dispatch/app/api/livekit-participant/route.ts +++ b/apps/dispatch/app/api/livekit-participant/route.ts @@ -13,9 +13,6 @@ export const GET = async (request: NextRequest) => { }, }); - if (!user || !user.permissions.includes("AUDIO_ADMIN")) - return Response.json({ message: "Missing permissions" }, { status: 401 }); - const rooms = await RoomManager.listRooms(); const roomsWithParticipants = rooms.map(async (room) => { diff --git a/apps/dispatch/app/pilot/_components/mrt/MRT_MESSAGE.png b/apps/dispatch/app/pilot/_components/mrt/MRT_MESSAGE.png new file mode 100644 index 00000000..7bf6a2d4 Binary files /dev/null and b/apps/dispatch/app/pilot/_components/mrt/MRT_MESSAGE.png differ diff --git a/apps/dispatch/app/pilot/_components/mrt/Mrt.tsx b/apps/dispatch/app/pilot/_components/mrt/Mrt.tsx index 52fc1a4a..5d271921 100644 --- a/apps/dispatch/app/pilot/_components/mrt/Mrt.tsx +++ b/apps/dispatch/app/pilot/_components/mrt/Mrt.tsx @@ -1,5 +1,6 @@ import { CSSProperties } from "react"; import MrtImage from "./MRT.png"; +import MrtMessageImage from "./MRT_MESSAGE.png"; import { useButtons } from "./useButtons"; import { useSounds } from "./useSounds"; import "./mrt.css"; @@ -18,6 +19,7 @@ const MRT_DISPLAYLINE_STYLES: CSSProperties = { }; export interface DisplayLineProps { + lineStyle?: CSSProperties; style?: CSSProperties; textLeft?: string; textMid?: string; @@ -31,12 +33,14 @@ const DisplayLine = ({ textMid, textRight, textSize, + lineStyle, }: DisplayLineProps) => { const INNER_TEXT_PARTS: CSSProperties = { fontFamily: "Melder", flex: "1", flexBasis: "auto", overflowWrap: "break-word", + ...lineStyle, }; return ( @@ -46,13 +50,12 @@ const DisplayLine = ({ fontFamily: "Famirids", display: "flex", flexWrap: "wrap", + ...style, }} > {textLeft} - - {textMid} - + {textMid} {textRight} ); @@ -61,7 +64,7 @@ const DisplayLine = ({ export const Mrt = () => { useSounds(); const { handleButton } = useButtons(); - const lines = useMrtStore((state) => state.lines); + const { lines, page } = useMrtStore((state) => state); return (
{ maxHeight: "100%", maxWidth: "100%", color: "white", - gridTemplateColumns: - "21.83% 4.43% 24.42% 18.08% 5.93% 1.98% 6.00% 1.69% 6.00% 9.35%", - gridTemplateRows: - "21.58% 11.87% 3.55% 5.00% 6.84% 0.53% 3.03% 11.84% 3.55% 11.84% 20.39%", + gridTemplateColumns: "21.83% 4.43% 24.42% 18.08% 5.93% 1.98% 6.00% 1.69% 6.00% 9.35%", + gridTemplateRows: "21.58% 11.87% 3.55% 5.00% 6.84% 0.53% 3.03% 11.84% 3.55% 11.84% 20.39%", }} > - MrtImage + )} + {page === "sds" && ( + MrtImage-Message + )} + +