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 }}