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*
|
yarn-error.log*
|
||||||
|
|
||||||
# local env files
|
# local env files
|
||||||
.env*.local
|
.env
|
||||||
|
*.local
|
||||||
|
|
||||||
# vercel
|
# vercel
|
||||||
.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';
|
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',
|
||||||
if (!response.ok) {
|
},
|
||||||
throw new Error('Failed to fetch VATSIM data');
|
distinct: ['cid'],
|
||||||
}
|
})
|
||||||
|
|
||||||
const data = await response.json();
|
return NextResponse.json(controllers)
|
||||||
|
|
||||||
// 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 })
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -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,61 +11,55 @@ 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));
|
||||||
});
|
});
|
||||||
|
|
||||||
// Process each controller
|
// Process each controller
|
||||||
for (const controller of czqmControllers) {
|
for (const controller of czqmControllers) {
|
||||||
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: {
|
||||||
if (error) {
|
lastSeen: new Date(),
|
||||||
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) {
|
||||||
|
@ -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 {
|
||||||
|
@ -4,33 +4,19 @@ 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) {
|
||||||
console.error("Error fetching controller data:", error);
|
console.error("Error fetching controller data:", error);
|
||||||
|
@ -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
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",
|
"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
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