more crap
This commit is contained in:
parent
a550879c5e
commit
be2febcb8d
3
.gitignore
vendored
3
.gitignore
vendored
@ -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
17
Cron-Docker/Dockerfile
Normal 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
1
Cron-Docker/crontab
Normal file
@ -0,0 +1 @@
|
||||
* * * * * curl localhost:3000/api/cron/update-controllers > /var/log/cron.log
|
23
app/api/controllers/[cid]/route.ts
Normal file
23
app/api/controllers/[cid]/route.ts
Normal 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 })
|
||||
}
|
||||
}
|
@ -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 })
|
||||
}
|
||||
}
|
@ -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) {
|
||||
|
@ -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 {
|
||||
|
@ -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) {
|
||||
|
@ -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
9
lib/prisma.ts
Normal 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
|
10
package.json
10
package.json
@ -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
25
prisma/schema.prisma
Normal 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])
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user