Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/ingestion/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ async function fetchVatsimData(): Promise<void> {
pilots: getPilotDelta(),
airports: getAirportDelta(),
controllers: getControllerDelta(),
timestamp: new Date(vatsimData.general.update_timestamp),
};
const gzDelta = await gzipAsync(JSON.stringify(delta));
rdsPub("ws:delta", gzDelta.toString("base64"));
Expand Down
4 changes: 3 additions & 1 deletion apps/web/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,14 @@ import type { Metadata } from "next";
import { Manrope } from "next/font/google";
import "./globals.css";
import "@/assets/images/sprites/freakflags.css";
import Footer from "@/components/Footer/Footer";
import Header from "@/components/Header/Header";
import Loader from "@/components/Loader/Loader";
import OMap from "@/components/Map/Map";
import BasePanel from "@/components/Panels/BasePanel";

export const metadata: Metadata = {
title: "simradar24",
title: "simradar21",
description: "VATSIM tracking service",
};

Expand All @@ -29,6 +30,7 @@ export default function RootLayout({
<Loader />
<OMap />
<BasePanel>{children}</BasePanel>
<Footer />
</body>
</html>
);
Expand Down
75 changes: 75 additions & 0 deletions apps/web/components/Footer/Footer.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
footer {
position: absolute;
left: 0rem;
right: 0rem;
bottom: 0rem;
height: 1.5rem;
display: flex;
align-items: center;
padding: 0 1rem;
box-sizing: border-box;
gap: 1rem;
background: linear-gradient(0deg, rgba(77, 95, 131, 0.3) 0%, rgba(77, 95, 131, 0) 120%);
border-top: 1px solid var(--color-border);
z-index: 1;
}

.footer-item {
background: white;
height: 100%;
border-left: 1px solid var(--color-border);
border-right: 1px solid var(--color-border);
box-sizing: border-box;
display: flex;
justify-content: center;
align-items: center;
padding: 0rem 10px;
font-size: 12px;
}

#footer-clients span {
font-weight: 700;
margin-right: 5px;
}

#footer-timestamp span {
margin-right: 8px;
height: 8px;
width: 8px;
background: var(--color-green);
border-radius: 50%;
animation: connected-blink 2s infinite;
}

@keyframes connected-blink {
0%,
50%,
100% {
opacity: 1;
}
25%,
75% {
opacity: 0;
}
}

#footer-github,
#footer-version {
background: none;
border: none;
color: white;
padding: 0rem;
}

#footer-github {
margin-left: auto;
}

#footer-github a {
font-weight: 700;
text-decoration: underline;
}

#footer-github a:hover {
color: var(--color-green);
}
70 changes: 70 additions & 0 deletions apps/web/components/Footer/Footer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
"use client";

import "./Footer.css";
import type { WsDelta } from "@sr24/types/vatsim";
import { useEffect, useState } from "react";
import useSWR from "swr";
import { fetchApi } from "@/utils/api";
import { wsClient } from "@/utils/ws";

interface Metrics {
connectedClients: number;
rateLimitedClients: number;
totalMessages: number;
avgMessagesPerClient: number;
timestamp: string;
}

const WS_URL = process.env.NEXT_PUBLIC_WEBSOCKET_URL || "ws://localhost:3002";

function getTimestamp(date: Date | string): string {
return `${new Date(date).toISOString().split("T")[1].split(".")[0]}z`;
}

export default function Footer() {
const { data: metrics, isLoading } = useSWR<Metrics>(`${WS_URL.replace("ws", "http")}/metrics`, fetchApi, { refreshInterval: 120_000 });

const [timestamp, setTimestamp] = useState<string>("");
const [stale, setStale] = useState<boolean>(false);

useEffect(() => {
setTimestamp(getTimestamp(new Date()));

let timeoutId: NodeJS.Timeout;
const handleMessage = (delta: WsDelta) => {
setTimestamp(getTimestamp(delta.timestamp));
setStale(false);

clearTimeout(timeoutId);
timeoutId = setTimeout(() => {
setStale(true);
}, 60_000);
};
wsClient.addListener(handleMessage);

return () => {
wsClient.removeListener(handleMessage);
};
}, []);

return (
<footer>
<div className="footer-item" id="footer-clients">
<span>{isLoading ? "..." : (metrics?.connectedClients ?? "0")}</span>visitors online
</div>
<div className="footer-item" id="footer-timestamp">
<span style={{ background: stale ? "var(--color-red)" : "", animationDuration: stale ? "1s" : "" }}></span>
{timestamp}
</div>
<div className="footer-item" id="footer-github">
Report a bug, request a feature, or send ❤️ on&nbsp;
<a href="https://github.com/sebastiankrll/simradar21" rel="noopener noreferrer" target="_blank">
GitHub
</a>
</div>
<div className="footer-item" id="footer-version">
v0.0.1
</div>
</footer>
);
}
23 changes: 17 additions & 6 deletions apps/websocket/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,21 @@ function checkRateLimit(clientContext: ClientContext): boolean {
const PORT = Number(process.env.WS_PORT) || 3002;
const HOST = process.env.WS_HOST || "localhost";

// Create HTTP server first
const setCorsHeaders = (res: any) => {
res.setHeader("Access-Control-Allow-Origin", "*");
res.setHeader("Access-Control-Allow-Methods", "GET, OPTIONS");
res.setHeader("Access-Control-Allow-Headers", "Content-Type");
};

const server = createServer((req: any, res: any) => {
setCorsHeaders(res);

if (req.method === "OPTIONS") {
res.writeHead(204);
res.end();
return;
}

if (req.url === "/health" && req.method === "GET") {
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify({ status: "ok", timestamp: new Date().toISOString() }));
Expand Down Expand Up @@ -112,7 +125,7 @@ wss.on("connection", (ws: WebSocket, _req: any) => {
};

clientContextMap.set(ws, clientContext);
// console.log(`✅ Client connected: ${clientId} from ${clientIp} (Total: ${clientContextMap.size})`);
// console.log(`✅ Client connected: ${clientId} (Total: ${clientContextMap.size})`);

ws.on("pong", () => {
clientContext.isAlive = true;
Expand Down Expand Up @@ -156,10 +169,7 @@ wss.on("connection", (ws: WebSocket, _req: any) => {

ws.on("close", () => {
clientContextMap.delete(ws);
// const duration = Date.now() - clientContext.connectedAt.getTime();
// console.log(
// `❌ Client disconnected: ${clientId} (connected for ${duration}ms, sent ${clientContext.messagesSent} messages, Total: ${clientContextMap.size})`,
// );
// console.log(`❌ Client disconnected: ${clientId} (Total: ${clientContextMap.size})`);
});
});

Expand All @@ -170,6 +180,7 @@ const heartbeatInterval = setInterval(() => {

if (!clientContext.isAlive) {
console.warn(`⏱️ Terminating inactive client: ${clientContext.id}`);
clientContextMap.delete(ws);
ws.terminate();
return;
}
Expand Down
1 change: 1 addition & 0 deletions packages/types/src/vatsim.ts
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,7 @@ export interface WsDelta {
pilots: PilotDelta;
controllers: ControllerDelta;
airports: AirportDelta;
timestamp: Date;
}

export interface VatsimEventData {
Expand Down