Skip to content

Commit f17b09d

Browse files
Merge pull request #40 from sebastiankrll/feature/footer
Feature/footer
2 parents 9182508 + 8423a8d commit f17b09d

File tree

6 files changed

+167
-7
lines changed

6 files changed

+167
-7
lines changed

apps/ingestion/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ async function fetchVatsimData(): Promise<void> {
5050
pilots: getPilotDelta(),
5151
airports: getAirportDelta(),
5252
controllers: getControllerDelta(),
53+
timestamp: new Date(vatsimData.general.update_timestamp),
5354
};
5455
const gzDelta = await gzipAsync(JSON.stringify(delta));
5556
rdsPub("ws:delta", gzDelta.toString("base64"));

apps/web/app/layout.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,14 @@ import type { Metadata } from "next";
22
import { Manrope } from "next/font/google";
33
import "./globals.css";
44
import "@/assets/images/sprites/freakflags.css";
5+
import Footer from "@/components/Footer/Footer";
56
import Header from "@/components/Header/Header";
67
import Loader from "@/components/Loader/Loader";
78
import OMap from "@/components/Map/Map";
89
import BasePanel from "@/components/Panels/BasePanel";
910

1011
export const metadata: Metadata = {
11-
title: "simradar24",
12+
title: "simradar21",
1213
description: "VATSIM tracking service",
1314
};
1415

@@ -29,6 +30,7 @@ export default function RootLayout({
2930
<Loader />
3031
<OMap />
3132
<BasePanel>{children}</BasePanel>
33+
<Footer />
3234
</body>
3335
</html>
3436
);
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
footer {
2+
position: absolute;
3+
left: 0rem;
4+
right: 0rem;
5+
bottom: 0rem;
6+
height: 1.5rem;
7+
display: flex;
8+
align-items: center;
9+
padding: 0 1rem;
10+
box-sizing: border-box;
11+
gap: 1rem;
12+
background: linear-gradient(0deg, rgba(77, 95, 131, 0.3) 0%, rgba(77, 95, 131, 0) 120%);
13+
border-top: 1px solid var(--color-border);
14+
z-index: 1;
15+
}
16+
17+
.footer-item {
18+
background: white;
19+
height: 100%;
20+
border-left: 1px solid var(--color-border);
21+
border-right: 1px solid var(--color-border);
22+
box-sizing: border-box;
23+
display: flex;
24+
justify-content: center;
25+
align-items: center;
26+
padding: 0rem 10px;
27+
font-size: 12px;
28+
}
29+
30+
#footer-clients span {
31+
font-weight: 700;
32+
margin-right: 5px;
33+
}
34+
35+
#footer-timestamp span {
36+
margin-right: 8px;
37+
height: 8px;
38+
width: 8px;
39+
background: var(--color-green);
40+
border-radius: 50%;
41+
animation: connected-blink 2s infinite;
42+
}
43+
44+
@keyframes connected-blink {
45+
0%,
46+
50%,
47+
100% {
48+
opacity: 1;
49+
}
50+
25%,
51+
75% {
52+
opacity: 0;
53+
}
54+
}
55+
56+
#footer-github,
57+
#footer-version {
58+
background: none;
59+
border: none;
60+
color: white;
61+
padding: 0rem;
62+
}
63+
64+
#footer-github {
65+
margin-left: auto;
66+
}
67+
68+
#footer-github a {
69+
font-weight: 700;
70+
text-decoration: underline;
71+
}
72+
73+
#footer-github a:hover {
74+
color: var(--color-green);
75+
}
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
"use client";
2+
3+
import "./Footer.css";
4+
import type { WsDelta } from "@sr24/types/vatsim";
5+
import { useEffect, useState } from "react";
6+
import useSWR from "swr";
7+
import { fetchApi } from "@/utils/api";
8+
import { wsClient } from "@/utils/ws";
9+
10+
interface Metrics {
11+
connectedClients: number;
12+
rateLimitedClients: number;
13+
totalMessages: number;
14+
avgMessagesPerClient: number;
15+
timestamp: string;
16+
}
17+
18+
const WS_URL = process.env.NEXT_PUBLIC_WEBSOCKET_URL || "ws://localhost:3002";
19+
20+
function getTimestamp(date: Date | string): string {
21+
return `${new Date(date).toISOString().split("T")[1].split(".")[0]}z`;
22+
}
23+
24+
export default function Footer() {
25+
const { data: metrics, isLoading } = useSWR<Metrics>(`${WS_URL.replace("ws", "http")}/metrics`, fetchApi, { refreshInterval: 120_000 });
26+
27+
const [timestamp, setTimestamp] = useState<string>("");
28+
const [stale, setStale] = useState<boolean>(false);
29+
30+
useEffect(() => {
31+
setTimestamp(getTimestamp(new Date()));
32+
33+
let timeoutId: NodeJS.Timeout;
34+
const handleMessage = (delta: WsDelta) => {
35+
setTimestamp(getTimestamp(delta.timestamp));
36+
setStale(false);
37+
38+
clearTimeout(timeoutId);
39+
timeoutId = setTimeout(() => {
40+
setStale(true);
41+
}, 60_000);
42+
};
43+
wsClient.addListener(handleMessage);
44+
45+
return () => {
46+
wsClient.removeListener(handleMessage);
47+
};
48+
}, []);
49+
50+
return (
51+
<footer>
52+
<div className="footer-item" id="footer-clients">
53+
<span>{isLoading ? "..." : (metrics?.connectedClients ?? "0")}</span>visitors online
54+
</div>
55+
<div className="footer-item" id="footer-timestamp">
56+
<span style={{ background: stale ? "var(--color-red)" : "", animationDuration: stale ? "1s" : "" }}></span>
57+
{timestamp}
58+
</div>
59+
<div className="footer-item" id="footer-github">
60+
Report a bug, request a feature, or send ❤️ on&nbsp;
61+
<a href="https://github.com/sebastiankrll/simradar21" rel="noopener noreferrer" target="_blank">
62+
GitHub
63+
</a>
64+
</div>
65+
<div className="footer-item" id="footer-version">
66+
v0.0.1
67+
</div>
68+
</footer>
69+
);
70+
}

apps/websocket/src/index.ts

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,21 @@ function checkRateLimit(clientContext: ClientContext): boolean {
4646
const PORT = Number(process.env.WS_PORT) || 3002;
4747
const HOST = process.env.WS_HOST || "localhost";
4848

49-
// Create HTTP server first
49+
const setCorsHeaders = (res: any) => {
50+
res.setHeader("Access-Control-Allow-Origin", "*");
51+
res.setHeader("Access-Control-Allow-Methods", "GET, OPTIONS");
52+
res.setHeader("Access-Control-Allow-Headers", "Content-Type");
53+
};
54+
5055
const server = createServer((req: any, res: any) => {
56+
setCorsHeaders(res);
57+
58+
if (req.method === "OPTIONS") {
59+
res.writeHead(204);
60+
res.end();
61+
return;
62+
}
63+
5164
if (req.url === "/health" && req.method === "GET") {
5265
res.writeHead(200, { "Content-Type": "application/json" });
5366
res.end(JSON.stringify({ status: "ok", timestamp: new Date().toISOString() }));
@@ -112,7 +125,7 @@ wss.on("connection", (ws: WebSocket, _req: any) => {
112125
};
113126

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

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

157170
ws.on("close", () => {
158171
clientContextMap.delete(ws);
159-
// const duration = Date.now() - clientContext.connectedAt.getTime();
160-
// console.log(
161-
// `❌ Client disconnected: ${clientId} (connected for ${duration}ms, sent ${clientContext.messagesSent} messages, Total: ${clientContextMap.size})`,
162-
// );
172+
// console.log(`❌ Client disconnected: ${clientId} (Total: ${clientContextMap.size})`);
163173
});
164174
});
165175

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

171181
if (!clientContext.isAlive) {
172182
console.warn(`⏱️ Terminating inactive client: ${clientContext.id}`);
183+
clientContextMap.delete(ws);
173184
ws.terminate();
174185
return;
175186
}

packages/types/src/vatsim.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -263,6 +263,7 @@ export interface WsDelta {
263263
pilots: PilotDelta;
264264
controllers: ControllerDelta;
265265
airports: AirportDelta;
266+
timestamp: Date;
266267
}
267268

268269
export interface VatsimEventData {

0 commit comments

Comments
 (0)