more crap

This commit is contained in:
Emma Ruby 2024-11-24 18:44:19 -06:00
parent a550879c5e
commit be2febcb8d
13 changed files with 132 additions and 166 deletions

3
.gitignore vendored
View File

@ -25,7 +25,8 @@ yarn-debug.log*
yarn-error.log* yarn-error.log*
# local env files # local env files
.env*.local .env
*.local
# vercel # vercel
.vercel .vercel

17
Cron-Docker/Dockerfile Normal file
View File

@ -0,0 +1,17 @@
FROM ubuntu:latest
# Install necessary software
RUN apt-get update && \
apt-get -y install cron curl
# Add crontab file
COPY crontab /etc/cron.d/crontab
# Give execution rights on the cron job
RUN chmod 0644 /etc/cron.d/crontab
# Create the log file to be able to run tail
RUN touch /var/log/cron.log
# Start the cron service
CMD cron && tail -f /var/log/cron.log

1
Cron-Docker/crontab Normal file
View File

@ -0,0 +1 @@
* * * * * curl localhost:3000/api/cron/update-controllers > /var/log/cron.log

View File

@ -0,0 +1,23 @@
import { NextResponse } from 'next/server'
import { prisma } from '@/lib/prisma'
export async function GET(
request: Request,
{ params }: { params: { cid: string } }
) {
try {
const sessions = await prisma.controllerSession.findMany({
where: {
cid: params.cid,
},
orderBy: {
lastSeen: 'desc',
},
})
return NextResponse.json(sessions)
} catch (error) {
console.error('Failed to fetch controller sessions:', error)
return NextResponse.json({ error: 'Failed to fetch controller sessions' }, { status: 500 })
}
}

View File

@ -1,104 +1,19 @@
import { NextResponse } from 'next/server'; import { NextResponse } from 'next/server'
import { prisma } from '@/lib/prisma'
interface Controller {
callsign: string;
frequency: string;
facility: number;
}
const facilityTypes = {
0: "OBS",
1: "FSS",
2: "DEL",
3: "GND",
4: "TWR",
5: "APP",
6: "CTR",
} as const;
const facilityLongNames = {
OBS: "Observer",
FSS: "Flight Service Station",
DEL: "Clearance Delivery",
GND: "Ground",
TWR: "Tower",
APP: "Approach/Departure",
CTR: "Centre",
} as const;
// Define FIR coverage for airports
const firCoverage: Record<string, string[]> = {
CZQM: ["CYHZ", "CYFC", "CYQM", "CYSJ", "CYZX", "CYYG", "CYYT", "CYQX", "CYYR", "LFVP", "CYQI", "CYAY", "CYDF", "CYJT"],
CZUL: ["CYZV"],
};
export async function GET() { export async function GET() {
try { try {
const response = await fetch('https://data.vatsim.net/v3/vatsim-data.json', { // Get unique controllers with their latest session
next: { revalidate: 60 } const controllers = await prisma.controllerSession.findMany({
}); orderBy: {
lastSeen: 'desc',
},
distinct: ['cid'],
})
if (!response.ok) { return NextResponse.json(controllers)
throw new Error('Failed to fetch VATSIM data');
}
const data = await response.json();
// First, get all controllers including CTR
const allControllers = data.controllers.map((controller: Controller) => {
const shortFacility = facilityTypes[controller.facility as keyof typeof facilityTypes] || "Unknown";
return {
callsign: controller.callsign,
frequency: controller.frequency,
facility: shortFacility,
facilityLong: facilityLongNames[shortFacility as keyof typeof facilityLongNames] || "Unknown",
facilityType: controller.facility,
airport: controller.callsign.split('_')[0]
};
});
// Separate CTR controllers
const ctrControllers = allControllers.filter(c => c.facilityType === 6);
// Get non-CTR controllers
const localControllers = allControllers.filter(c => c.facilityType < 6 && c.facilityType > 0 && !c.callsign.includes("ATIS"));
// Group local controllers by airport
const controllersByAirport = localControllers.reduce((acc: any, controller) => {
if (!acc[controller.airport]) {
acc[controller.airport] = [];
}
acc[controller.airport].push(controller);
return acc;
}, {});
// Sort controllers at each airport by facility type (higher number = higher priority)
Object.values(controllersByAirport).forEach((controllers: any) => {
controllers.sort((a: any, b: any) => b.facilityType - a.facilityType);
});
// Add CTR coverage for airports without local controllers
ctrControllers.forEach(ctr => {
const fir = ctr.callsign.split('_')[0];
const coveredAirports = firCoverage[fir] || [];
coveredAirports.forEach(airport => {
// Only add CTR if no local controllers are available
if (!controllersByAirport[airport] || controllersByAirport[airport].length === 0) {
controllersByAirport[airport] = [{
callsign: ctr.callsign,
frequency: ctr.frequency,
facility: ctr.facility,
facilityLong: "Centre (Top-down)",
airport: airport
}];
}
});
});
return NextResponse.json(controllersByAirport);
} catch (error) { } catch (error) {
console.error(error); console.error('Failed to fetch controller data:', error)
return NextResponse.json({ error: 'Failed to fetch controller data' }, { status: 500 }); return NextResponse.json({ error: 'Failed to fetch controller data' }, { status: 500 })
} }
} }

View File

@ -1,6 +1,5 @@
import { createClient } from '@supabase/supabase-js'
import { NextResponse } from 'next/server' import { NextResponse } from 'next/server'
import { prisma } from '@/lib/prisma'
const facilityTypes = { const facilityTypes = {
0: "OBS", 0: "OBS",
@ -12,24 +11,18 @@ const facilityTypes = {
6: "CTR", 6: "CTR",
} as const; } as const;
// Define FIR coverage const CZQM_AIRPORTS = ["CYHZ", "CYFC", "CYQM", "CYSJ", "CYZX", "CYYG", "CYYT", "CYQX", "CYYR", "LFVP", "CYQI", "CYAY", "CYDF", "CYJT", "BOS"];
const CZQM_AIRPORTS = ["CYHZ", "CYFC", "CYQM", "CYSJ", "CYZX", "CYYG", "CYYT", "CYQX", "CYYR", "LFVP", "CYQI", "CYAY", "CYDF", "CYJT"];
export async function GET() { export async function GET() {
try { try {
// Fetch current VATSIM data
const response = await fetch('https://data.vatsim.net/v3/vatsim-data.json'); const response = await fetch('https://data.vatsim.net/v3/vatsim-data.json');
if (!response.ok) throw new Error('Failed to fetch VATSIM data'); if (!response.ok) throw new Error('Failed to fetch VATSIM data');
const data = await response.json(); const data = await response.json();
const supabase = createClient(process.env.NEXT_PUBLIC_SUPABASE_URL, process.env.NEXT_SECRET_SUPABASE_KEY)
// Filter controllers in CZQM airspace // Filter controllers in CZQM airspace
const czqmControllers = data.controllers.filter((controller: any) => { const czqmControllers = data.controllers.filter((controller: any) => {
const callsign = controller.callsign; const callsign = controller.callsign;
// console.log(callsign)
return callsign.startsWith('CZQM_') || return callsign.startsWith('CZQM_') ||
callsign.startsWith('CZQX_') ||
CZQM_AIRPORTS.some(airport => callsign.startsWith(airport)); CZQM_AIRPORTS.some(airport => callsign.startsWith(airport));
}); });
@ -38,35 +31,35 @@ export async function GET() {
const facilityType = facilityTypes[controller.facility as keyof typeof facilityTypes]; const facilityType = facilityTypes[controller.facility as keyof typeof facilityTypes];
const airport = CZQM_AIRPORTS.find(ap => controller.callsign.startsWith(ap)) || 'CZQM'; const airport = CZQM_AIRPORTS.find(ap => controller.callsign.startsWith(ap)) || 'CZQM';
// Insert or update controller session await prisma.controllerSession.upsert({
const { error } = await supabase where: {
.from('controller_sessions') id: parseInt(controller.cid),
.upsert({ },
cid: controller.cid, create: {
cid: String(controller.cid),
name: controller.name, name: controller.name,
callsign: controller.callsign, callsign: controller.callsign,
facility_type: facilityType, facilityType: facilityType,
frequency: controller.frequency, frequency: controller.frequency,
airport: airport, airport: airport,
last_seen: new Date().toISOString(), lastSeen: new Date(),
logon_time: controller.logon_time, logonTime: new Date(controller.logon_time),
},
update: {
lastSeen: new Date(),
},
}); });
if (error) {
console.error('Error updating controller session:', error);
}
} }
// Clean up old sessions (controllers who have logged off) // Clean up old sessions
const fiveMinutesAgo = new Date(Date.now() - 5 * 60 * 1000).toISOString(); const fiveMinutesAgo = new Date(Date.now() - 5 * 60 * 1000);
const { error: cleanupError } = await supabase await prisma.controllerSession.deleteMany({
.from('controller_sessions') where: {
.delete() lastSeen: {
.lt('last_seen', fiveMinutesAgo); lt: fiveMinutesAgo,
},
if (cleanupError) { },
console.error('Error cleaning up old sessions:', cleanupError); });
}
return NextResponse.json({ success: true }); return NextResponse.json({ success: true });
} catch (error) { } catch (error) {

View File

@ -5,25 +5,19 @@ import { useParams } from "next/navigation";
import { Card } from "@/components/ui/card"; import { Card } from "@/components/ui/card";
import { Header } from "@/components/header"; import { Header } from "@/components/header";
import { ControllerProfile } from "@/components/controller-profile"; import { ControllerProfile } from "@/components/controller-profile";
import { createClient } from "@/lib/supabase/client";
export default function ControllerProfilePage() { export default function ControllerProfilePage() {
const params = useParams(); const params = useParams();
const [sessions, setSessions] = useState<any[]>([]); const [sessions, setSessions] = useState<any[]>([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const supabase = createClient();
useEffect(() => { useEffect(() => {
async function loadControllerData() { async function loadControllerData() {
try { try {
const { data, error } = await supabase const response = await fetch(`/api/controllers/${params.cid}`);
.from('controller_sessions') if (!response.ok) throw new Error('Failed to fetch controller data');
.select('*') const data = await response.json();
.eq('cid', params.cid) setSessions(data);
.order('last_seen', { ascending: false });
if (error) throw error;
setSessions(data || []);
} catch (error) { } catch (error) {
console.error("Error fetching controller data:", error); console.error("Error fetching controller data:", error);
} finally { } finally {

View File

@ -4,32 +4,18 @@ import { useEffect, useState } from "react";
import { Card } from "@/components/ui/card"; import { Card } from "@/components/ui/card";
import { Header } from "@/components/header"; import { Header } from "@/components/header";
import { ControllerTable } from "@/components/controller-table"; import { ControllerTable } from "@/components/controller-table";
import { createClient } from "@/lib/supabase/client"; import { prisma } from '@/lib/prisma'
export default function ControllersPage() { export default function ControllersPage() {
const [controllers, setControllers] = useState<any[]>([]); const [controllers, setControllers] = useState<any[]>([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const supabase = createClient();
useEffect(() => { useEffect(() => {
async function loadControllers() { async function loadControllers() {
try { try {
// Get unique controllers with their latest session const response = await fetch('/api/controllers');
const { data, error } = await supabase if (!response.ok) throw new Error('Failed to fetch METAR data');
.from('controller_sessions') const uniqueControllers = await response.json();
.select('*')
.order('last_seen', { ascending: false });
if (error) throw error;
// Group by CID and take the most recent session
const uniqueControllers = data.reduce((acc: any[], curr) => {
const existingIndex = acc.findIndex(c => c.cid === curr.cid);
if (existingIndex === -1) {
acc.push(curr);
}
return acc;
}, []);
setControllers(uniqueControllers); setControllers(uniqueControllers);
} catch (error) { } catch (error) {

BIN
bun.lockb

Binary file not shown.

View File

@ -53,10 +53,10 @@ export function ControllerTable({ data, loading }: ControllerTableProps) {
<TableCell>{controller.airport}</TableCell> <TableCell>{controller.airport}</TableCell>
<TableCell>{controller.frequency}</TableCell> <TableCell>{controller.frequency}</TableCell>
<TableCell> <TableCell>
{formatDistanceToNow(new Date(controller.last_seen), { addSuffix: true })} {formatDistanceToNow(new Date(controller.lastSeen), { addSuffix: true })}
</TableCell> </TableCell>
<TableCell> <TableCell>
{formatSessionDuration(controller.logon_time, controller.last_seen)} {formatSessionDuration(controller.logonTime, controller.lastSeen)}
</TableCell> </TableCell>
</TableRow> </TableRow>
))} ))}

9
lib/prisma.ts Normal file
View File

@ -0,0 +1,9 @@
import { PrismaClient } from '@prisma/client'
const globalForPrisma = globalThis as unknown as {
prisma: PrismaClient | undefined
}
export const prisma = globalForPrisma.prisma ?? new PrismaClient()
if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma

View File

@ -6,11 +6,13 @@
"dev": "next dev", "dev": "next dev",
"build": "next build", "build": "next build",
"start": "next start", "start": "next start",
"lint": "next lint" "lint": "next lint",
"postinstall": "prisma generate"
}, },
"dependencies": { "dependencies": {
"@hookform/resolvers": "^3.9.0", "@hookform/resolvers": "^3.9.0",
"@next/swc-wasm-nodejs": "13.5.1", "@next/swc-wasm-nodejs": "13.5.1",
"@prisma/client": "^5.10.2",
"@radix-ui/react-accordion": "^1.2.0", "@radix-ui/react-accordion": "^1.2.0",
"@radix-ui/react-alert-dialog": "^1.1.1", "@radix-ui/react-alert-dialog": "^1.1.1",
"@radix-ui/react-aspect-ratio": "^1.1.0", "@radix-ui/react-aspect-ratio": "^1.1.0",
@ -38,8 +40,6 @@
"@radix-ui/react-toggle": "^1.1.0", "@radix-ui/react-toggle": "^1.1.0",
"@radix-ui/react-toggle-group": "^1.1.0", "@radix-ui/react-toggle-group": "^1.1.0",
"@radix-ui/react-tooltip": "^1.1.2", "@radix-ui/react-tooltip": "^1.1.2",
"@supabase/auth-helpers-nextjs": "^0.9.0",
"@supabase/supabase-js": "^2.39.0",
"@types/node": "20.6.2", "@types/node": "20.6.2",
"@types/react": "18.2.22", "@types/react": "18.2.22",
"@types/react-dom": "18.2.7", "@types/react-dom": "18.2.7",
@ -63,12 +63,14 @@
"react-resizable-panels": "^2.1.3", "react-resizable-panels": "^2.1.3",
"recharts": "^2.12.7", "recharts": "^2.12.7",
"sonner": "^1.5.0", "sonner": "^1.5.0",
"supabase": "^1.223.10",
"tailwind-merge": "^2.5.2", "tailwind-merge": "^2.5.2",
"tailwindcss": "3.3.3", "tailwindcss": "3.3.3",
"tailwindcss-animate": "^1.0.7", "tailwindcss-animate": "^1.0.7",
"typescript": "5.2.2", "typescript": "5.2.2",
"vaul": "^0.9.9", "vaul": "^0.9.9",
"zod": "^3.23.8" "zod": "^3.23.8"
},
"devDependencies": {
"prisma": "^5.10.2"
} }
} }

25
prisma/schema.prisma Normal file
View File

@ -0,0 +1,25 @@
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model ControllerSession {
id Int @id @default(autoincrement())
cid String
name String
callsign String
facilityType String
frequency String
airport String
lastSeen DateTime @map("last_seen")
logonTime DateTime @map("logon_time")
createdAt DateTime @default(now()) @map("created_at")
@@map("controller_sessions")
@@index([cid])
@@index([lastSeen])
}