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

View File

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

View File

@ -4,32 +4,18 @@ import { useEffect, useState } from "react";
import { Card } from "@/components/ui/card";
import { Header } from "@/components/header";
import { ControllerTable } from "@/components/controller-table";
import { createClient } from "@/lib/supabase/client";
import { prisma } from '@/lib/prisma'
export default function ControllersPage() {
const [controllers, setControllers] = useState<any[]>([]);
const [loading, setLoading] = useState(true);
const supabase = createClient();
useEffect(() => {
async function loadControllers() {
try {
// Get unique controllers with their latest session
const { data, error } = await supabase
.from('controller_sessions')
.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;
}, []);
const response = await fetch('/api/controllers');
if (!response.ok) throw new Error('Failed to fetch METAR data');
const uniqueControllers = await response.json();
setControllers(uniqueControllers);
} 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.frequency}</TableCell>
<TableCell>
{formatDistanceToNow(new Date(controller.last_seen), { addSuffix: true })}
{formatDistanceToNow(new Date(controller.lastSeen), { addSuffix: true })}
</TableCell>
<TableCell>
{formatSessionDuration(controller.logon_time, controller.last_seen)}
{formatSessionDuration(controller.logonTime, controller.lastSeen)}
</TableCell>
</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",
"build": "next build",
"start": "next start",
"lint": "next lint"
"lint": "next lint",
"postinstall": "prisma generate"
},
"dependencies": {
"@hookform/resolvers": "^3.9.0",
"@next/swc-wasm-nodejs": "13.5.1",
"@prisma/client": "^5.10.2",
"@radix-ui/react-accordion": "^1.2.0",
"@radix-ui/react-alert-dialog": "^1.1.1",
"@radix-ui/react-aspect-ratio": "^1.1.0",
@ -38,8 +40,6 @@
"@radix-ui/react-toggle": "^1.1.0",
"@radix-ui/react-toggle-group": "^1.1.0",
"@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/react": "18.2.22",
"@types/react-dom": "18.2.7",
@ -63,12 +63,14 @@
"react-resizable-panels": "^2.1.3",
"recharts": "^2.12.7",
"sonner": "^1.5.0",
"supabase": "^1.223.10",
"tailwind-merge": "^2.5.2",
"tailwindcss": "3.3.3",
"tailwindcss-animate": "^1.0.7",
"typescript": "5.2.2",
"vaul": "^0.9.9",
"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])
}