diff --git a/app/Console/Commands/UpdateDepartedFlights.php b/app/Console/Commands/UpdateDepartedFlights.php new file mode 100644 index 0000000..ba7ff28 --- /dev/null +++ b/app/Console/Commands/UpdateDepartedFlights.php @@ -0,0 +1,78 @@ +utc(); + $oneHourAgo = $now->copy()->subHours(1); + + $userFlights = UserFlight::whereBetween('arrival_date', [ + $oneHourAgo->toDateTimeString(), + $now->toDateTimeString(), + ]) + ->where('auto_update', true) + ->whereNotNull('flight_number') + ->get(); + + $this->info("Found {$userFlights->count()} flights."); + + foreach ($userFlights as $flight) { + // Split "QF22" into ["QF", "22"] + preg_match('/^([A-Z]{2,3})(\d+)$/i', $flight->flight_number, $matches); + + if (empty($matches)) { + $this->warn("Could not parse flight number: {$flight->flight_number}"); + continue; + } + + $airlineCode = strtoupper($matches[1]); + $flightNumber = $matches[2]; + + $arrivalDate = $flight->arrival_date->setTimezone($flight->arrivalAirport->timezone); + $year = $arrivalDate->year; + $month = $arrivalDate->month; + $day = $arrivalDate->day; + + $url = "https://www.flightstats.com/v2/api-next/flight-tracker/{$airlineCode}/{$flightNumber}/{$year}/{$month}/{$day}"; + + $response = Http::get($url); + + if (!$response->successful()) { + $this->warn("Failed to fetch data for {$flight->flight_number}: HTTP {$response->status()}"); + continue; + } + + $data = $response->json(); + + $flightData = $data['data'] ?? []; + + if (empty($flightData)) { + $this->warn("No flight data returned for {$airlineCode}{$flightNumber}"); + continue; + } + + $tailNumber = $flightData['positional']['flexTrack']['tailNumber'] ?? null; + $estimatedDepartureUtc = $flightData['schedule']['estimatedActualDepartureUTC'] ?? null; + $estimatedArrivalUtc = $flightData['schedule']['estimatedActualArrivalUTC'] ?? null; + $equipmentCode = $flightData['additionalFlightInfo']['equipment']['iata'] ?? null; + + $this->info("Flight {$airlineCode}{$flightNumber} — Tail: {$tailNumber}, Equipment: {$equipmentCode}"); + $this->info("Departure: {$estimatedDepartureUtc} | Arrival: {$estimatedArrivalUtc}"); + + } + } +} diff --git a/app/Http/Controllers/FlightController.php b/app/Http/Controllers/FlightController.php index e98d0ae..68ebb1d 100644 --- a/app/Http/Controllers/FlightController.php +++ b/app/Http/Controllers/FlightController.php @@ -210,7 +210,7 @@ class FlightController extends Controller { $this->authorize('delete', $flight); - $snapshot = $this->flightSnapshot($flight->id); + $snapshot = $flight->snapshot($flight->id); if(now()->utc()->isBefore($flight->departure_date)){ $action = 'flight_deleted'; diff --git a/app/Models/UserFlight.php b/app/Models/UserFlight.php index 4a2cef9..a6d6ca9 100644 --- a/app/Models/UserFlight.php +++ b/app/Models/UserFlight.php @@ -145,7 +145,7 @@ class UserFlight extends Model protected function distance(): Attribute { return Attribute::make( - get: fn() => $this->calculateGreatCircleDistance() + get: fn() => round($this->calculateGreatCircleDistance()) ); } @@ -224,7 +224,7 @@ class UserFlight extends Model public function liveryUrl(): Attribute{ return Attribute::make( get: function () { - if($this->airline) { + if($this->airline && $this->aircraft) { $fileName = "{$this->airline->internal_name}_{$this->aircraft->designator}.png"; $file = public_path("img/liveries/generated/$fileName"); diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh new file mode 100644 index 0000000..fa5327b --- /dev/null +++ b/docker/entrypoint.sh @@ -0,0 +1,10 @@ +#!/bin/sh +set -e + +php artisan config:clear +php artisan config:cache +php artisan migrate --force + +crond -f -l 8 & +php-fpm -D +nginx -g 'daemon off;' diff --git a/dockerfile b/dockerfile index 5f947f2..07c8de4 100644 --- a/dockerfile +++ b/dockerfile @@ -1,10 +1,8 @@ FROM php:8.4-fpm-alpine -# Install dependencies -RUN apk add --no-cache nginx curl zip unzip git postgresql-dev nodejs npm \ +RUN apk add --no-cache nginx curl zip unzip git postgresql-dev nodejs npm dcron \ && docker-php-ext-install pdo pdo_pgsql pgsql opcache -# Install Composer COPY --from=composer:latest /usr/bin/composer /usr/bin/composer WORKDIR /var/www @@ -19,6 +17,12 @@ COPY docker/nginx.conf /etc/nginx/nginx.conf RUN chown -R www-data:www-data /var/www/storage /var/www/bootstrap/cache +RUN echo "* * * * * cd /var/www && php artisan schedule:run" \ + > /etc/crontabs/root + +COPY docker/entrypoint.sh /entrypoint.sh +RUN chmod +x /entrypoint.sh + EXPOSE 80 -CMD ["sh", "-c", "php-fpm -D && nginx -g 'daemon off;'"] +CMD ["/entrypoint.sh"] diff --git a/resources/js/Components/FlightsGoneBy/AircraftToolTip.vue b/resources/js/Components/FlightsGoneBy/AircraftToolTip.vue index 992c322..76bc064 100644 --- a/resources/js/Components/FlightsGoneBy/AircraftToolTip.vue +++ b/resources/js/Components/FlightsGoneBy/AircraftToolTip.vue @@ -2,21 +2,14 @@ import { Aircraft } from "@/Types/types"; import GlassTooltip from "@/Components/FlightsGoneBy/GlassTooltip.vue"; import InlineBadge from "@/Components/FlightsGoneBy/InlineBadge.vue"; +import WakeTurbulence from "@/Components/FlightsGoneBy/WakeTurbulence.vue"; defineProps<{ aircraft: Aircraft showTooltips?: boolean }>() -function formatWtc(wtc: string): string { - switch (wtc.toUpperCase()) { - case 'L': return 'Light' - case 'M': return 'Medium' - case 'H': return 'Heavy' - case 'J': return 'Super' - default: return wtc - } -} + function formatEngineType(type: string): string { return type.replace(/_/g, ' ').replace(/\b\w/g, c => c.toUpperCase()) @@ -41,7 +34,7 @@ function formatEngineType(type: string): string { {{aircraft.designator }} - {{ formatWtc(aircraft.wtc) }} + diff --git a/resources/js/Components/FlightsGoneBy/AllianceLogo.vue b/resources/js/Components/FlightsGoneBy/AllianceLogo.vue index 769f6e1..178f279 100644 --- a/resources/js/Components/FlightsGoneBy/AllianceLogo.vue +++ b/resources/js/Components/FlightsGoneBy/AllianceLogo.vue @@ -17,7 +17,7 @@ const size = computed(() => props.size ? props.size + 'px' : '30px'); diff --git a/resources/js/Components/FlightsGoneBy/Panels/AirlinePanel.vue b/resources/js/Components/FlightsGoneBy/Panels/AirlinePanel.vue new file mode 100644 index 0000000..b14ba93 --- /dev/null +++ b/resources/js/Components/FlightsGoneBy/Panels/AirlinePanel.vue @@ -0,0 +1,65 @@ + + + + + diff --git a/resources/js/Components/FlightsGoneBy/Panels/AirportPanel.vue b/resources/js/Components/FlightsGoneBy/Panels/AirportPanel.vue new file mode 100644 index 0000000..b3725bd --- /dev/null +++ b/resources/js/Components/FlightsGoneBy/Panels/AirportPanel.vue @@ -0,0 +1,37 @@ + + + + + diff --git a/resources/js/Components/FlightsGoneBy/Panels/DetailRow.vue b/resources/js/Components/FlightsGoneBy/Panels/DetailRow.vue new file mode 100644 index 0000000..3ee27dc --- /dev/null +++ b/resources/js/Components/FlightsGoneBy/Panels/DetailRow.vue @@ -0,0 +1,48 @@ + + + + + diff --git a/resources/js/Components/FlightsGoneBy/Panels/DetailRows.vue b/resources/js/Components/FlightsGoneBy/Panels/DetailRows.vue new file mode 100644 index 0000000..fd9cf8a --- /dev/null +++ b/resources/js/Components/FlightsGoneBy/Panels/DetailRows.vue @@ -0,0 +1,20 @@ + + + + + diff --git a/resources/js/Components/FlightsGoneBy/Panels/Panel.vue b/resources/js/Components/FlightsGoneBy/Panels/Panel.vue new file mode 100644 index 0000000..44fe63f --- /dev/null +++ b/resources/js/Components/FlightsGoneBy/Panels/Panel.vue @@ -0,0 +1,35 @@ + + + + + diff --git a/resources/js/Components/FlightsGoneBy/Panels/PanelHeader.vue b/resources/js/Components/FlightsGoneBy/Panels/PanelHeader.vue new file mode 100644 index 0000000..2e3ec70 --- /dev/null +++ b/resources/js/Components/FlightsGoneBy/Panels/PanelHeader.vue @@ -0,0 +1,17 @@ + + + + + diff --git a/resources/js/Components/FlightsGoneBy/Panels/PanelSubHeader.vue b/resources/js/Components/FlightsGoneBy/Panels/PanelSubHeader.vue new file mode 100644 index 0000000..b2a135c --- /dev/null +++ b/resources/js/Components/FlightsGoneBy/Panels/PanelSubHeader.vue @@ -0,0 +1,15 @@ + + + + + diff --git a/resources/js/Components/FlightsGoneBy/Panels/RoutePanel.vue b/resources/js/Components/FlightsGoneBy/Panels/RoutePanel.vue new file mode 100644 index 0000000..f50676b --- /dev/null +++ b/resources/js/Components/FlightsGoneBy/Panels/RoutePanel.vue @@ -0,0 +1,26 @@ + + + + + diff --git a/resources/js/Components/FlightsGoneBy/WakeTurbulence.vue b/resources/js/Components/FlightsGoneBy/WakeTurbulence.vue new file mode 100644 index 0000000..06681be --- /dev/null +++ b/resources/js/Components/FlightsGoneBy/WakeTurbulence.vue @@ -0,0 +1,22 @@ + + + + + diff --git a/resources/js/Pages/UserFlight.vue b/resources/js/Pages/UserFlight.vue index 7e8fefa..28d09dd 100644 --- a/resources/js/Pages/UserFlight.vue +++ b/resources/js/Pages/UserFlight.vue @@ -4,9 +4,12 @@ import {Head} from "@inertiajs/vue3"; import ProfileLayout from "@/Components/FlightsGoneBy/ProfileLayout.vue"; import {Achievement, Flight, User, UserAchievement} from "@/Types/types"; import BoardingPass from "@/Components/FlightsGoneBy/BoardingPass.vue"; -import AllianceLogo from "@/Components/FlightsGoneBy/AllianceLogo.vue"; -import FlightMap from "@/Components/FlightsGoneBy/FlightMap.vue"; -import AirlineLogo from "@/Components/FlightsGoneBy/AirlineLogo.vue"; +import Panel from "@/Components/FlightsGoneBy/Panels/Panel.vue"; +import AirportPanel from "@/Components/FlightsGoneBy/Panels/AirportPanel.vue"; +import AirlinePanel from "@/Components/FlightsGoneBy/Panels/AirlinePanel.vue"; +import AircraftPanel from "@/Components/FlightsGoneBy/Panels/AircraftPanel.vue"; +import RoutePanel from "@/Components/FlightsGoneBy/Panels/RoutePanel.vue"; +import DetailRows from "@/Components/FlightsGoneBy/Panels/DetailRows.vue"; defineOptions({ layout: MainLayout }) @@ -19,466 +22,60 @@ const props = defineProps<{ diff --git a/resources/js/Pages/UserProfile.vue b/resources/js/Pages/UserProfile.vue index 850f3e9..f59a54b 100644 --- a/resources/js/Pages/UserProfile.vue +++ b/resources/js/Pages/UserProfile.vue @@ -135,7 +135,7 @@ function switchView(view: ProfileView) { - + comment(Inspiring::quote()); })->purpose('Display an inspiring quote'); + +Schedule::command('app:update-departed-flights')->hourly()->runInBackground();