diff --git a/apps/dispatch-server/index.ts b/apps/dispatch-server/index.ts index 4cd7c1e0..bb8751f5 100644 --- a/apps/dispatch-server/index.ts +++ b/apps/dispatch-server/index.ts @@ -10,6 +10,7 @@ import router from "routes/router"; import cors from "cors"; import { handleSendMessage } from "socket-events/send-message"; import { handleConnectPilot } from "socket-events/connect-pilot"; +import { handleConnectDesktop } from "socket-events/connect-desktop"; const app = express(); const server = createServer(app); @@ -21,8 +22,10 @@ export const io = new Server(server, { io.use(jwtMiddleware); io.on("connection", (socket) => { + console.log("New socket connection", socket.id); socket.on("connect-dispatch", handleConnectDispatch(socket, io)); socket.on("connect-pilot", handleConnectPilot(socket, io)); + socket.on("connect-desktop", handleConnectDesktop(socket, io)); socket.on("send-message", handleSendMessage(socket, io)); }); diff --git a/apps/dispatch-server/socket-events/connect-desktop.ts b/apps/dispatch-server/socket-events/connect-desktop.ts new file mode 100644 index 00000000..c7e3edc2 --- /dev/null +++ b/apps/dispatch-server/socket-events/connect-desktop.ts @@ -0,0 +1,15 @@ +import { User } from "@repo/db"; +import { Socket, Server } from "socket.io"; + +export const handleConnectDesktop = + (socket: Socket, io: Server) => (socket: Socket) => { + const user = socket.data.user as User; + console.log("connect-desktop", user.publicId); + socket.join(`user:${user.id}`); + socket.join(`desktop:${user.id}`); + + socket.on("ptt", (data) => { + console.log("ptt", data); + socket.to(`user:${user.id}`).emit("ptt", data); + }); + }; diff --git a/apps/dispatch-server/socket-events/connect-pilot.ts b/apps/dispatch-server/socket-events/connect-pilot.ts index b2224241..322a2918 100644 --- a/apps/dispatch-server/socket-events/connect-pilot.ts +++ b/apps/dispatch-server/socket-events/connect-pilot.ts @@ -66,7 +66,6 @@ export const handleConnectPilot = stationId: parseInt(stationId), posLat: 51.45, posLng: 9.77, - simulatorConnected: true, }, }); diff --git a/apps/dispatch/app/pilot/_components/navbar/Audio.tsx b/apps/dispatch/app/_components/Audio.tsx similarity index 98% rename from apps/dispatch/app/pilot/_components/navbar/Audio.tsx rename to apps/dispatch/app/_components/Audio.tsx index 786a5053..89df26b3 100644 --- a/apps/dispatch/app/pilot/_components/navbar/Audio.tsx +++ b/apps/dispatch/app/_components/Audio.tsx @@ -64,7 +64,7 @@ export const Audio = () => { className={cn( "btn btn-sm btn-soft border-none hover:bg-inherit", !isTalking && "bg-transparent hover:bg-sky-400/20", - isTalking && "bg-red-500 hover:bg-red-600", + isTalking && "bg-green-700 hover:bg-green-600", state === "disconnected" && "bg-red-500 hover:bg-red-500", state === "error" && "bg-red-500 hover:bg-red-500", state === "connecting" && diff --git a/apps/dispatch/app/_store/audioStore.ts b/apps/dispatch/app/_store/audioStore.ts index 3690ca2e..87432e52 100644 --- a/apps/dispatch/app/_store/audioStore.ts +++ b/apps/dispatch/app/_store/audioStore.ts @@ -1,3 +1,4 @@ +import { dispatchSocket } from "dispatch/socket"; import { serverApi } from "helpers/axios"; import { handleActiveSpeakerChange, @@ -7,12 +8,14 @@ import { handleTrackUnsubscribed, } from "helpers/liveKitEventHandler"; import { ConnectionQuality, Room, RoomEvent } from "livekit-client"; +import { pilotSocket } from "pilot/socket"; import { create } from "zustand"; let interval: NodeJS.Timeout; type TalkState = { isTalking: boolean; + source: string; state: "connecting" | "connected" | "disconnected" | "error"; message: string | null; connectionQuality: ConnectionQuality; @@ -33,10 +36,17 @@ export const useAudioStore = create((set, get) => ({ isTalking: false, message: null, state: "disconnected", + source: "", remoteParticipants: 0, connectionQuality: ConnectionQuality.Unknown, room: null, - toggleTalking: () => set((state) => ({ isTalking: !state.isTalking })), + toggleTalking: () => { + const { room, isTalking } = get(); + if (!room) return; + room.localParticipant.setMicrophoneEnabled(!isTalking); + + set((state) => ({ isTalking: !state.isTalking })); + }, connect: async (roomName) => { set({ state: "connecting" }); console.log("Connecting to room: ", roomName); @@ -78,6 +88,7 @@ export const useAudioStore = create((set, get) => ({ await room.connect(url, token); console.log(room); set({ room }); + interval = setInterval(() => { set({ remoteParticipants: @@ -97,3 +108,28 @@ export const useAudioStore = create((set, get) => ({ get().room?.disconnect(); }, })); + +interface PTTData { + shouldTransmit: boolean; + source: string; +} + +const handlePTT = (data: PTTData) => { + const { shouldTransmit, source } = data; + const { room } = useAudioStore.getState(); + if (!room) return; + + useAudioStore.setState({ + isTalking: shouldTransmit, + source, + }); + + if (shouldTransmit) { + room.localParticipant.setMicrophoneEnabled(true); + } else { + room.localParticipant.setMicrophoneEnabled(false); + } +}; + +pilotSocket.on("ptt", handlePTT); +dispatchSocket.on("ptt", handlePTT); diff --git a/apps/dispatch/app/api/position-log/route.ts b/apps/dispatch/app/api/position-log/route.ts index 47d7eb4c..4db50a06 100644 --- a/apps/dispatch/app/api/position-log/route.ts +++ b/apps/dispatch/app/api/position-log/route.ts @@ -1,24 +1,41 @@ -import { Prisma, prisma } from "@repo/db"; +import { PositionLog, Prisma, prisma, User } from "@repo/db"; import { getServerSession } from "api/auth/[...nextauth]/auth"; +import { verify } from "jsonwebtoken"; export const PUT = async (req: Request) => { const session = await getServerSession(); - if (!session) + const token = req.headers.get("authorization")?.split(" ")[1]; + if (!token) + return Response.json({ message: "Missing token" }, { status: 401 }); + + const payload = await new Promise((resolve, reject) => { + verify(token, process.env.NEXTAUTH_HUB_SECRET as string, (err, decoded) => { + if (err) { + reject(err); + } else { + resolve(decoded as User); + } + }); + }); + + if (!session && !payload) return Response.json({ message: "Unauthorized" }, { status: 401 }); + + const userId = session?.user.id || payload.id; const { position, h145 } = (await req.json()) as { - position: Prisma.PositionLogCreateInput; + position: PositionLog; h145: boolean; }; + console.log("position", userId); if (!position) { - return Response.json( - { message: "Missing id or position" }, - { status: 400 }, - ); + return Response.json({ message: "Missing id or position" }); } + console.log("position", position); + const activeAircraft = await prisma.connectedAircraft.findFirst({ where: { - userId: session.user.id, + userId, logoutTime: null, }, orderBy: { @@ -36,7 +53,10 @@ export const PUT = async (req: Request) => { ); } const positionLog = await prisma.positionLog.create({ - data: position, + data: { + ...position, + userId, + }, }); await prisma.connectedAircraft.update({ @@ -44,6 +64,7 @@ export const PUT = async (req: Request) => { id: activeAircraft?.id, }, data: { + userId, lastHeartbeat: new Date(), posAlt: positionLog.alt, posLat: positionLog.lat, diff --git a/apps/dispatch/app/dispatch/_components/map/AircraftMarker.tsx b/apps/dispatch/app/dispatch/_components/map/AircraftMarker.tsx index a2568240..330f2ecf 100644 --- a/apps/dispatch/app/dispatch/_components/map/AircraftMarker.tsx +++ b/apps/dispatch/app/dispatch/_components/map/AircraftMarker.tsx @@ -30,6 +30,7 @@ import { ConnectedAircraft, Station } from "@repo/db"; import { useQuery } from "@tanstack/react-query"; import { getConnectedAircraftsAPI } from "querys/aircrafts"; import { getMissionsAPI } from "querys/missions"; +import { checkSimulatorConnected } from "helpers/simulatorConnected"; export const FMS_STATUS_COLORS: { [key: string]: string } = { "0": "rgb(140,10,10)", @@ -394,7 +395,7 @@ const AircraftMarker = ({ return ( - {aircraft.simulatorConnected && ( + {checkSimulatorConnected(aircraft.lastHeartbeat) && ( { const { data: aircrafts } = useQuery({ queryKey: ["aircrafts"], queryFn: getConnectedAircraftsAPI, + refetchInterval: 10000, }); return ( diff --git a/apps/dispatch/app/dispatch/_components/map/_components/AircraftMarkerTabs.tsx b/apps/dispatch/app/dispatch/_components/map/_components/AircraftMarkerTabs.tsx index 18a635aa..32945aa2 100644 --- a/apps/dispatch/app/dispatch/_components/map/_components/AircraftMarkerTabs.tsx +++ b/apps/dispatch/app/dispatch/_components/map/_components/AircraftMarkerTabs.tsx @@ -22,7 +22,6 @@ import { Mountain, Navigation, RadioTower, - SirenIcon, Sunset, TextSearch, } from "lucide-react"; diff --git a/apps/dispatch/app/dispatch/_components/map/_components/MarkerCluster.tsx b/apps/dispatch/app/dispatch/_components/map/_components/MarkerCluster.tsx index 2b079aaa..b0da9b0d 100644 --- a/apps/dispatch/app/dispatch/_components/map/_components/MarkerCluster.tsx +++ b/apps/dispatch/app/dispatch/_components/map/_components/MarkerCluster.tsx @@ -11,6 +11,7 @@ import { MISSION_STATUS_TEXT_COLORS, } from "dispatch/_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, useState } from "react"; @@ -102,7 +103,7 @@ const PopupContent = ({ ))} {aircrafts - .filter((a) => a.simulatorConnected) + .filter((a) => checkSimulatorConnected(a.lastHeartbeat)) .map((aircraft) => (
{ const zoom = map.getZoom(); let newCluster: typeof cluster = []; aircrafts - ?.filter((a) => a.simulatorConnected) + ?.filter((a) => checkSimulatorConnected(a.lastHeartbeat)) .forEach((aircraft) => { const lat = aircraft.posLat!; const lng = aircraft.posLng!; diff --git a/apps/dispatch/app/dispatch/_components/navbar/Navbar.tsx b/apps/dispatch/app/dispatch/_components/navbar/Navbar.tsx index 80a1c628..1bdabca4 100644 --- a/apps/dispatch/app/dispatch/_components/navbar/Navbar.tsx +++ b/apps/dispatch/app/dispatch/_components/navbar/Navbar.tsx @@ -2,7 +2,7 @@ import { Connection } from "./_components/Connection"; import { ThemeSwap } from "./_components/ThemeSwap"; -import { Audio } from "./_components/Audio"; +import { Audio } from "../../../_components/Audio"; import { useState } from "react"; import { ExitIcon, ExternalLinkIcon } from "@radix-ui/react-icons"; import Link from "next/link"; diff --git a/apps/dispatch/app/dispatch/_components/navbar/_components/Audio.tsx b/apps/dispatch/app/dispatch/_components/navbar/_components/Audio.tsx deleted file mode 100644 index 381dd979..00000000 --- a/apps/dispatch/app/dispatch/_components/navbar/_components/Audio.tsx +++ /dev/null @@ -1,143 +0,0 @@ -"use client"; - -import { useEffect, useState } from "react"; -import { useDispatchConnectionStore } from "_store/dispatch/connectionStore"; -import { - Disc, - Mic, - PlugZap, - ServerCrash, - ShieldQuestion, - Signal, - SignalLow, - SignalMedium, - WifiOff, - ZapOff, -} from "lucide-react"; -import { useAudioStore } from "_store/audioStore"; -import { cn } from "helpers/cn"; -import { ConnectionQuality } from "livekit-client"; -import { ROOMS } from "_data/livekitRooms"; - -export const Audio = () => { - const connection = useDispatchConnectionStore(); - const { - isTalking, - toggleTalking, - connect, - state, - connectionQuality, - disconnect, - remoteParticipants, - room, - message, - } = useAudioStore(); - const [selectedRoom, setSelectedRoom] = useState("LST_01"); - - useEffect(() => { - const joinRoom = async () => { - if (connection.status != "connected") return; - if (state === "connected") return; - connect(selectedRoom); - }; - - joinRoom(); - - return () => { - disconnect(); - }; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [connection.status]); - - return ( - <> -
- {state === "error" && ( -
{message}
- )} - - - {state === "connected" && ( -
- - {connectionQuality === ConnectionQuality.Excellent && ( - - )} - {connectionQuality === ConnectionQuality.Good && ( - - )} - {connectionQuality === ConnectionQuality.Poor && ( - - )} - {connectionQuality === ConnectionQuality.Lost && ( - - )} - {connectionQuality === ConnectionQuality.Unknown && ( - - )} -
- {remoteParticipants} -
-
-
    - {ROOMS.map((r) => ( -
  • - -
  • - ))} -
  • - -
  • -
-
- )} -
- - ); -}; diff --git a/apps/dispatch/app/dispatch/_components/navbar/_components/Connection.tsx b/apps/dispatch/app/dispatch/_components/navbar/_components/Connection.tsx index 50f75f61..cc048797 100644 --- a/apps/dispatch/app/dispatch/_components/navbar/_components/Connection.tsx +++ b/apps/dispatch/app/dispatch/_components/navbar/_components/Connection.tsx @@ -93,7 +93,9 @@ export const ConnectionBtn = () => { }} className="btn btn-soft btn-info" > - Verbinden + {connection.status == "disconnected" + ? "Verbinden" + : connection.status} )} diff --git a/apps/dispatch/app/helpers/simulatorConnected.ts b/apps/dispatch/app/helpers/simulatorConnected.ts new file mode 100644 index 00000000..9c49ab80 --- /dev/null +++ b/apps/dispatch/app/helpers/simulatorConnected.ts @@ -0,0 +1,2 @@ +export const checkSimulatorConnected = (date: Date) => + date && Date.now() - new Date(date).getTime() <= 30_000; diff --git a/apps/dispatch/app/pilot/_components/mrt/Mrt.tsx b/apps/dispatch/app/pilot/_components/mrt/Mrt.tsx index 711c0eff..e2d37c3b 100644 --- a/apps/dispatch/app/pilot/_components/mrt/Mrt.tsx +++ b/apps/dispatch/app/pilot/_components/mrt/Mrt.tsx @@ -74,7 +74,6 @@ export const Mrt = () => { width: "auto", maxHeight: "100%", maxWidth: "100%", - overflow: "hidden", color: "white", gridTemplateColumns: "21.83% 4.43% 24.42% 18.08% 5.93% 1.98% 6.00% 1.69% 6.00% 9.35%", diff --git a/apps/dispatch/app/pilot/_components/navbar/Connection.tsx b/apps/dispatch/app/pilot/_components/navbar/Connection.tsx index f911917a..e7a0edfe 100644 --- a/apps/dispatch/app/pilot/_components/navbar/Connection.tsx +++ b/apps/dispatch/app/pilot/_components/navbar/Connection.tsx @@ -147,7 +147,9 @@ export const ConnectionBtn = () => { }} className="btn btn-soft btn-info" > - Verbinden + {connection.status == "disconnected" + ? "Verbinden" + : connection.status} )} diff --git a/apps/dispatch/app/pilot/_components/navbar/Navbar.tsx b/apps/dispatch/app/pilot/_components/navbar/Navbar.tsx index ae38974e..53cfc32e 100644 --- a/apps/dispatch/app/pilot/_components/navbar/Navbar.tsx +++ b/apps/dispatch/app/pilot/_components/navbar/Navbar.tsx @@ -2,7 +2,7 @@ import { Connection } from "./Connection"; import { ThemeSwap } from "./ThemeSwap"; -import { Audio } from "./Audio"; +import { Audio } from "../../../_components/Audio"; import { useState } from "react"; import { ExitIcon, ExternalLinkIcon } from "@radix-ui/react-icons"; import Link from "next/link"; diff --git a/apps/dispatch/app/pilot/layout.tsx b/apps/dispatch/app/pilot/layout.tsx index 98865f30..6c01af08 100644 --- a/apps/dispatch/app/pilot/layout.tsx +++ b/apps/dispatch/app/pilot/layout.tsx @@ -4,7 +4,7 @@ import { redirect } from "next/navigation"; import { getServerSession } from "../api/auth/[...nextauth]/auth"; export const metadata: Metadata = { - title: "VAR v2: Disponent", + title: "VAR v2: Pilot", description: "Die neue VAR Leitstelle.", }; diff --git a/apps/dispatch/app/pilot/page.tsx b/apps/dispatch/app/pilot/page.tsx index 223ddcdd..d39e0cdc 100644 --- a/apps/dispatch/app/pilot/page.tsx +++ b/apps/dispatch/app/pilot/page.tsx @@ -3,7 +3,6 @@ import { Mrt } from "pilot/_components/mrt/Mrt"; import { Chat } from "../_components/left/Chat"; import { Report } from "../_components/left/Report"; -import { usePilotConnectionStore } from "_store/pilot/connectionStore"; import { Dme } from "pilot/_components/dme/Dme"; const DispatchPage = () => { @@ -19,8 +18,12 @@ const DispatchPage = () => {
- - +
+ +
+
+ +
); diff --git a/apps/dispatch/package.json b/apps/dispatch/package.json index a558f783..4b63ed20 100644 --- a/apps/dispatch/package.json +++ b/apps/dispatch/package.json @@ -19,6 +19,7 @@ "@tailwindcss/postcss": "^4.0.14", "@tanstack/react-query": "^5.75.4", "axios": "^1.9.0", + "jsonwebtoken": "^9.0.2", "leaflet": "^1.9.4", "livekit-client": "^2.9.7", "livekit-server-sdk": "^2.10.2", diff --git a/grafana/grafana.db b/grafana/grafana.db index b3da090f..c4b943f6 100644 Binary files a/grafana/grafana.db and b/grafana/grafana.db differ diff --git a/package-lock.json b/package-lock.json index 8ecdc9b1..77946f43 100644 --- a/package-lock.json +++ b/package-lock.json @@ -29,6 +29,7 @@ "@tailwindcss/postcss": "^4.0.14", "@tanstack/react-query": "^5.75.4", "axios": "^1.9.0", + "jsonwebtoken": "^9.0.2", "leaflet": "^1.9.4", "livekit-client": "^2.9.7", "livekit-server-sdk": "^2.10.2", diff --git a/packages/database/prisma/schema/connectedAircraft.prisma b/packages/database/prisma/schema/connectedAircraft.prisma index 31f8c28a..d310e13c 100644 --- a/packages/database/prisma/schema/connectedAircraft.prisma +++ b/packages/database/prisma/schema/connectedAircraft.prisma @@ -11,7 +11,6 @@ model ConnectedAircraft { posSpeed Int? posHeading Int? simulator String? - simulatorConnected Boolean @default(false) posH145active Boolean @default(false) stationId Int loginTime DateTime @default(now())