added MRT sds image

This commit is contained in:
PxlLoewe
2025-06-07 00:34:30 -07:00
parent 6be3f70371
commit dd53110500
16 changed files with 1853 additions and 136 deletions

View File

@@ -327,9 +327,6 @@ router.post("/:id/validate-hpg", async (req, res) => {
res.json({ res.json({
message: "HPG validierung gestartet", message: "HPG validierung gestartet",
}); });
console.log(
`HPG Validation for ${user?.publicId} (${mission?.hpgSelectedMissionString}) started`,
);
io.to(`desktop:${activeAircraftinMission?.userId}`).emit("hpg-validation", { io.to(`desktop:${activeAircraftinMission?.userId}`).emit("hpg-validation", {
missionId: parseInt(id), missionId: parseInt(id),
userId: req.user?.id, userId: req.user?.id,

View File

@@ -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({ const connectedAircraftEntry = await prisma.connectedAircraft.create({
data: { data: {
publicUser: getPublicUser(user) as any, publicUser: getPublicUser(user) as any,
esimatedLogoutTime: parsedLogoffDate?.toISOString() || null, esimatedLogoutTime: parsedLogoffDate?.toISOString() || null,
lastHeartbeat: new Date().toISOString(),
userId: userId, userId: userId,
loginTime: new Date().toISOString(), loginTime: nowPlus2h.toISOString(),
stationId: parseInt(stationId), stationId: parseInt(stationId),
lastHeartbeat:
process.env.environment === "development" ? nowPlus2h.toISOString() : undefined,
posLat: randomPos?.lat,
posLng: randomPos?.lng,
}, },
}); });

View File

@@ -15,7 +15,6 @@ import { ConnectedAircraft, Station } from "@repo/db";
import { useQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
import { getConnectedAircraftPositionLogAPI, getConnectedAircraftsAPI } from "_querys/aircrafts"; import { getConnectedAircraftPositionLogAPI, getConnectedAircraftsAPI } from "_querys/aircrafts";
import { getMissionsAPI } from "_querys/missions"; import { getMissionsAPI } from "_querys/missions";
import { checkSimulatorConnected } from "_helpers/simulatorConnected";
import { FMS_STATUS_COLORS, FMS_STATUS_TEXT_COLORS } from "_helpers/fmsStatusColors"; import { FMS_STATUS_COLORS, FMS_STATUS_TEXT_COLORS } from "_helpers/fmsStatusColors";
const AircraftPopupContent = ({ const AircraftPopupContent = ({
@@ -384,14 +383,12 @@ export const AircraftLayer = () => {
const { data: aircrafts } = useQuery({ const { data: aircrafts } = useQuery({
queryKey: ["aircrafts"], queryKey: ["aircrafts"],
queryFn: getConnectedAircraftsAPI, queryFn: getConnectedAircraftsAPI,
refetchInterval: 10000, refetchInterval: 10_000,
}); });
return ( return (
<> <>
{aircrafts {aircrafts?.map((aircraft) => {
?.filter((a) => checkSimulatorConnected(a.lastHeartbeat))
?.map((aircraft) => {
return <AircraftMarker key={aircraft.id} aircraft={aircraft} />; return <AircraftMarker key={aircraft.id} aircraft={aircraft} />;
})} })}
</> </>

View File

@@ -1,5 +1,5 @@
"use client"; "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 { FMS_STATUS_COLORS, FMS_STATUS_TEXT_COLORS } from "_helpers/fmsStatusColors";
import { import {
ConnectedAircraft, ConnectedAircraft,
@@ -13,7 +13,7 @@ import {
Station, Station,
} from "@repo/db"; } from "@repo/db";
import { toast } from "react-hot-toast"; 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 { editConnectedAircraftAPI } from "_querys/aircrafts";
import { useDispatchConnectionStore } from "_store/dispatch/connectionStore"; import { useDispatchConnectionStore } from "_store/dispatch/connectionStore";
import { cn } from "_helpers/cn"; import { cn } from "_helpers/cn";
@@ -39,7 +39,9 @@ import {
TextSearch, TextSearch,
} from "lucide-react"; } from "lucide-react";
import { useSession } from "next-auth/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 = ({ const FMSStatusHistory = ({
aircraft, aircraft,
@@ -216,14 +218,35 @@ const RettungsmittelTab = ({
aircraft: ConnectedAircraft & { Station: Station }; aircraft: ConnectedAircraft & { Station: Station };
}) => { }) => {
const station = aircraft.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 ( return (
<div className="p-4 text-base-content"> <div className="p-4 text-base-content">
<ul className="text-base-content font-semibold"> <ul className="text-base-content font-semibold">
<li className="flex items-center gap-2 mb-1"> <li className="flex items-center gap-2 mb-1">
<Component size={16} /> Aktuelle Rufgruppe: LST_01 <Component size={16} /> Aktuelle Rufgruppe: {livekitUser?.roomName || "Nicht verbunden"}
</li> </li>
<li className="flex items-center gap-2 mb-1"> <li className="flex items-center gap-2 mb-1">
<RadioTower size={16} /> Leitstellenbereich: Florian Berlin <RadioTower size={16} /> Leitstellenbereich: {lstName || station.bosRadioArea}
</li> </li>
</ul> </ul>
<div className="divider mt-0 mb-0" /> <div className="divider mt-0 mb-0" />
@@ -348,7 +371,7 @@ const SDSTab = ({
onClick={() => setIsChatOpen(true)} onClick={() => setIsChatOpen(true)}
> >
<span className="flex items-center gap-2"> <span className="flex items-center gap-2">
<Plus size={18} /> Notiz hinzufügen <Plus size={18} /> SDS senden
</span> </span>
</button> </button>
) : ( ) : (
@@ -379,6 +402,7 @@ const SDSTab = ({
}, },
}) })
.then(() => { .then(() => {
toast.success("SDS-Nachricht gesendet");
setIsChatOpen(false); setIsChatOpen(false);
setNote(""); setNote("");
}); });

View File

@@ -6,7 +6,6 @@ import { useMapStore } from "_store/mapStore";
import { FMS_STATUS_COLORS, FMS_STATUS_TEXT_COLORS } from "_helpers/fmsStatusColors"; import { FMS_STATUS_COLORS, FMS_STATUS_TEXT_COLORS } from "_helpers/fmsStatusColors";
import { MISSION_STATUS_COLORS, MISSION_STATUS_TEXT_COLORS } from "_components/map/MissionMarkers"; import { MISSION_STATUS_COLORS, MISSION_STATUS_TEXT_COLORS } from "_components/map/MissionMarkers";
import { cn } from "_helpers/cn"; import { cn } from "_helpers/cn";
import { checkSimulatorConnected } from "_helpers/simulatorConnected";
import { getConnectedAircraftsAPI } from "_querys/aircrafts"; import { getConnectedAircraftsAPI } from "_querys/aircrafts";
import { getMissionsAPI } from "_querys/missions"; import { getMissionsAPI } from "_querys/missions";
import { useEffect, useMemo, useState } from "react"; import { useEffect, useMemo, useState } from "react";
@@ -96,9 +95,7 @@ const PopupContent = ({
</div> </div>
); );
})} })}
{aircrafts {aircrafts.map((aircraft) => (
.filter((a) => checkSimulatorConnected(a.lastHeartbeat))
.map((aircraft) => (
<div <div
key={aircraft.id} key={aircraft.id}
className="relative w-auto inline-flex items-center gap-2 text-nowrap cursor-pointer" className="relative w-auto inline-flex items-center gap-2 text-nowrap cursor-pointer"
@@ -141,6 +138,7 @@ export const MarkerCluster = () => {
const { data: aircrafts } = useQuery({ const { data: aircrafts } = useQuery({
queryKey: ["aircrafts"], queryKey: ["aircrafts"],
queryFn: getConnectedAircraftsAPI, queryFn: getConnectedAircraftsAPI,
refetchInterval: 10_000,
}); });
const dispatcherConnected = useDispatchConnectionStore((s) => s.status) === "connected"; const dispatcherConnected = useDispatchConnectionStore((s) => s.status) === "connected";
@@ -178,9 +176,7 @@ export const MarkerCluster = () => {
lat: number; lat: number;
lng: number; lng: number;
}[] = []; }[] = [];
aircrafts aircrafts?.forEach((aircraft) => {
?.filter((a) => checkSimulatorConnected(a.lastHeartbeat))
.forEach((aircraft) => {
const lat = aircraft.posLat!; const lat = aircraft.posLat!;
const lng = aircraft.posLng!; const lng = aircraft.posLng!;

View File

@@ -35,14 +35,14 @@ export default function AdminPanel() {
refetchInterval: 10000, refetchInterval: 10000,
}); });
const { data: livekitRooms } = useQuery({ const { data: livekitRooms } = useQuery({
queryKey: ["connected-audio-users"], queryKey: ["livekit-rooms"],
queryFn: () => getLivekitRooms(), queryFn: () => getLivekitRooms(),
refetchInterval: 10000, refetchInterval: 10000,
}); });
const kickLivekitParticipantMutation = useMutation({ const kickLivekitParticipantMutation = useMutation({
mutationFn: kickLivekitParticipant, mutationFn: kickLivekitParticipant,
onSuccess: () => { onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["connected-audio-users"] }); queryClient.invalidateQueries({ queryKey: ["livekit-rooms"] });
}, },
}); });
const editUSerMutation = useMutation({ const editUSerMutation = useMutation({
@@ -95,8 +95,6 @@ export default function AdminPanel() {
return !pilot && !fDispatcher; return !pilot && !fDispatcher;
}); });
console.log("Livekit Rooms", livekitRooms);
const modalRef = useRef<HTMLDialogElement>(null); const modalRef = useRef<HTMLDialogElement>(null);
return ( return (

View File

@@ -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
}

View File

@@ -1,2 +1,7 @@
export const checkSimulatorConnected = (date: Date) => import { ConnectedAircraft } from "@repo/db";
date && Date.now() - new Date(date).getTime() <= 3000_000;
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;
};

View File

@@ -1,13 +1,14 @@
import { ConnectedAircraft, PositionLog, Prisma, PublicUser, Station } from "@repo/db"; import { ConnectedAircraft, PositionLog, Prisma, PublicUser, Station } from "@repo/db";
import axios from "axios"; import axios from "axios";
import { serverApi } from "_helpers/axios"; import { serverApi } from "_helpers/axios";
import { checkSimulatorConnected } from "_helpers/simulatorConnected";
export const getConnectedAircraftsAPI = async () => { export const getConnectedAircraftsAPI = async () => {
const res = await axios.get<(ConnectedAircraft & { Station: Station })[]>("/api/aircrafts"); // return only connected aircrafts const res = await axios.get<(ConnectedAircraft & { Station: Station })[]>("/api/aircrafts"); // return only connected aircrafts
if (res.status !== 200) { if (res.status !== 200) {
throw new Error("Failed to fetch stations"); throw new Error("Failed to fetch stations");
} }
return res.data; return res.data.filter((a) => checkSimulatorConnected(a));
}; };
export const editConnectedAircraftAPI = async ( export const editConnectedAircraftAPI = async (

View File

@@ -122,7 +122,7 @@ export const useMrtStore = create<MrtStore>(
}, },
{ textLeft: "ILS VAR#", textSize: "3" }, { textLeft: "ILS VAR#", textSize: "3" },
{ {
textLeft: "new status received", textLeft: "empfangen",
style: { fontWeight: "bold" }, style: { fontWeight: "bold" },
textSize: "4", textSize: "4",
}, },
@@ -136,7 +136,7 @@ export const useMrtStore = create<MrtStore>(
page: "sds", page: "sds",
lines: [ lines: [
{ {
textLeft: `neue SDS-Nachricht`, textLeft: `SDS-Nachricht`,
style: { fontWeight: "bold" }, style: { fontWeight: "bold" },
textSize: "2", textSize: "2",
}, },

View File

@@ -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 rooms = await RoomManager.listRooms();
const roomsWithParticipants = rooms.map(async (room) => { const roomsWithParticipants = rooms.map(async (room) => {

Binary file not shown.

After

Width:  |  Height:  |  Size: 322 KiB

View File

@@ -1,5 +1,6 @@
import { CSSProperties } from "react"; import { CSSProperties } from "react";
import MrtImage from "./MRT.png"; import MrtImage from "./MRT.png";
import MrtMessageImage from "./MRT_MESSAGE.png";
import { useButtons } from "./useButtons"; import { useButtons } from "./useButtons";
import { useSounds } from "./useSounds"; import { useSounds } from "./useSounds";
import "./mrt.css"; import "./mrt.css";
@@ -18,6 +19,7 @@ const MRT_DISPLAYLINE_STYLES: CSSProperties = {
}; };
export interface DisplayLineProps { export interface DisplayLineProps {
lineStyle?: CSSProperties;
style?: CSSProperties; style?: CSSProperties;
textLeft?: string; textLeft?: string;
textMid?: string; textMid?: string;
@@ -31,12 +33,14 @@ const DisplayLine = ({
textMid, textMid,
textRight, textRight,
textSize, textSize,
lineStyle,
}: DisplayLineProps) => { }: DisplayLineProps) => {
const INNER_TEXT_PARTS: CSSProperties = { const INNER_TEXT_PARTS: CSSProperties = {
fontFamily: "Melder", fontFamily: "Melder",
flex: "1", flex: "1",
flexBasis: "auto", flexBasis: "auto",
overflowWrap: "break-word", overflowWrap: "break-word",
...lineStyle,
}; };
return ( return (
@@ -46,13 +50,12 @@ const DisplayLine = ({
fontFamily: "Famirids", fontFamily: "Famirids",
display: "flex", display: "flex",
flexWrap: "wrap", flexWrap: "wrap",
...style, ...style,
}} }}
> >
<span style={INNER_TEXT_PARTS}>{textLeft}</span> <span style={INNER_TEXT_PARTS}>{textLeft}</span>
<span style={{ textAlign: "center", ...INNER_TEXT_PARTS }}> <span style={{ textAlign: "center", ...INNER_TEXT_PARTS }}>{textMid}</span>
{textMid}
</span>
<span style={{ textAlign: "end", ...INNER_TEXT_PARTS }}>{textRight}</span> <span style={{ textAlign: "end", ...INNER_TEXT_PARTS }}>{textRight}</span>
</div> </div>
); );
@@ -61,7 +64,7 @@ const DisplayLine = ({
export const Mrt = () => { export const Mrt = () => {
useSounds(); useSounds();
const { handleButton } = useButtons(); const { handleButton } = useButtons();
const lines = useMrtStore((state) => state.lines); const { lines, page } = useMrtStore((state) => state);
return ( return (
<div <div
@@ -75,12 +78,11 @@ export const Mrt = () => {
maxHeight: "100%", maxHeight: "100%",
maxWidth: "100%", maxWidth: "100%",
color: "white", color: "white",
gridTemplateColumns: gridTemplateColumns: "21.83% 4.43% 24.42% 18.08% 5.93% 1.98% 6.00% 1.69% 6.00% 9.35%",
"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%",
gridTemplateRows:
"21.58% 11.87% 3.55% 5.00% 6.84% 0.53% 3.03% 11.84% 3.55% 11.84% 20.39%",
}} }}
> >
{page !== "sds" && (
<Image <Image
src={MrtImage} src={MrtImage}
alt="MrtImage" alt="MrtImage"
@@ -91,6 +93,24 @@ export const Mrt = () => {
gridArea: "1 / 1 / 13 / 13", gridArea: "1 / 1 / 13 / 13",
}} }}
/> />
)}
{page === "sds" && (
<Image
src={MrtMessageImage}
alt="MrtImage-Message"
style={{
zIndex: 0,
height: "100%",
width: "100%",
gridArea: "1 / 1 / 13 / 13",
}}
/>
)}
<button
onClick={handleButton("home")}
style={{ gridArea: "2 / 4 / 3 / 5", ...MRT_BUTTON_STYLES }}
/>
<button <button
onClick={handleButton("1")} onClick={handleButton("1")}
style={{ gridArea: "2 / 5 / 3 / 6", ...MRT_BUTTON_STYLES }} style={{ gridArea: "2 / 5 / 3 / 6", ...MRT_BUTTON_STYLES }}
@@ -135,25 +155,50 @@ export const Mrt = () => {
{lines[0] && ( {lines[0] && (
<DisplayLine <DisplayLine
{...lines[0]} {...lines[0]}
style={{ style={
page === "sds"
? {
gridArea: "2 / 3 / 3 / 4",
marginLeft: "9px",
marginTop: "auto",
...MRT_DISPLAYLINE_STYLES,
...lines[0]?.style,
}
: {
gridArea: "4 / 3 / 5 / 4", gridArea: "4 / 3 / 5 / 4",
marginLeft: "9px", marginLeft: "9px",
marginTop: "auto", marginTop: "auto",
...MRT_DISPLAYLINE_STYLES, ...MRT_DISPLAYLINE_STYLES,
...lines[0]?.style, ...lines[0]?.style,
}} }
}
/> />
)} )}
{lines[1] && ( {lines[1] && (
<DisplayLine <DisplayLine
lineStyle={{
overflowX: "hidden",
maxHeight: "100%",
overflowY: "auto",
}}
{...lines[1]} {...lines[1]}
style={{ style={
page === "sds"
? {
gridArea: "4 / 2 / 10 / 4",
marginLeft: "3px",
...MRT_DISPLAYLINE_STYLES,
...lines[1].style,
}
: {
gridArea: "5 / 3 / 7 / 4", gridArea: "5 / 3 / 7 / 4",
marginLeft: "3px", marginLeft: "3px",
marginTop: "auto", marginTop: "auto",
...MRT_DISPLAYLINE_STYLES, ...MRT_DISPLAYLINE_STYLES,
...lines[1].style, ...lines[1].style,
}} }
}
/> />
)} )}
{lines[2] && ( {lines[2] && (

View File

@@ -24,7 +24,7 @@ export const useButtons = () => {
const { page, setPage } = useMrtStore((state) => state); const { page, setPage } = useMrtStore((state) => state);
const handleButton = const handleButton =
(button: "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" | "0") => () => { (button: "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" | "0" | "home") => () => {
if (connectionStatus !== "connected") return; if (connectionStatus !== "connected") return;
if (!station) return; if (!station) return;
if (!connectedAircraft?.id) return; if (!connectedAircraft?.id) return;
@@ -40,7 +40,6 @@ export const useButtons = () => {
button === "9" || button === "9" ||
button === "0" button === "0"
) { ) {
if (page !== "home") return;
setPage({ page: "sending-status", station }); setPage({ page: "sending-status", station });
setTimeout(async () => { setTimeout(async () => {
@@ -56,6 +55,8 @@ export const useButtons = () => {
fmsStatus: button, fmsStatus: button,
}); });
}, 1000); }, 1000);
} else {
setPage({ page: "home", fmsStatus: connectedAircraft.fmsStatus || "6", station });
} }
}; };

View File

@@ -23,9 +23,15 @@
"@repo/typescript-config": "workspace:*", "@repo/typescript-config": "workspace:*",
"@tailwindcss/postcss": "^4.1.8", "@tailwindcss/postcss": "^4.1.8",
"@tanstack/react-query": "^5.79.0", "@tanstack/react-query": "^5.79.0",
"@turf/turf": "^7.2.0",
"@types/jsonwebtoken": "^9.0.9", "@types/jsonwebtoken": "^9.0.9",
"@types/leaflet": "^1.9.18",
"@types/node": "^22.15.29",
"@types/react": "^19.1.6",
"@types/react-dom": "^19.1.5",
"axios": "^1.9.0", "axios": "^1.9.0",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"daisyui": "^5.0.43",
"geojson": "^0.5.0", "geojson": "^0.5.0",
"i": "^0.3.7", "i": "^0.3.7",
"jsonwebtoken": "^9.0.2", "jsonwebtoken": "^9.0.2",
@@ -47,15 +53,9 @@
"socket.io-client": "^4.8.1", "socket.io-client": "^4.8.1",
"tailwind-merge": "^3.3.0", "tailwind-merge": "^3.3.0",
"tailwindcss": "^4.1.8", "tailwindcss": "^4.1.8",
"typescript": "^5.8.3",
"zod": "^3.25.46", "zod": "^3.25.46",
"zustand": "^5.0.5", "zustand": "^5.0.5",
"zustand-sync-tabs": "^0.2.2", "zustand-sync-tabs": "^0.2.2"
"@types/leaflet": "^1.9.18", }
"@types/node": "^22.15.29",
"@types/react": "^19.1.6",
"@types/react-dom": "^19.1.5",
"daisyui": "^5.0.43",
"typescript": "^5.8.3"
},
"devDependencies": {}
} }

1617
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff