diff --git a/app/Console/Commands/GetAircraftInfoForArrivedFlights.php b/app/Console/Commands/GetAircraftInfoForArrivedFlights.php
new file mode 100644
index 0000000..1f6a390
--- /dev/null
+++ b/app/Console/Commands/GetAircraftInfoForArrivedFlights.php
@@ -0,0 +1,217 @@
+utc();
+
+ $userFlights = UserFlight::where('arrival_date', '<=', $now->copy()->subHour()->toDateTimeString())
+ ->where('auto_update', true)
+ ->whereNotNull('flight_number')
+ ->get();
+
+ $this->info("Found {$userFlights->count()} flights.");
+
+ foreach ($userFlights as $flight) {
+ $this->processFlight($flight);
+ }
+ }
+
+ protected function processFlight(UserFlight $flight): void
+ {
+ preg_match('/^([A-Z]{2,3})(\d+)$/i', $flight->flight_number, $matches);
+
+ // Case 1: unparseable flight number on our end — disable and move on, no notification.
+ if (empty($matches)) {
+ $this->warn("Could not parse flight number: {$flight->flight_number}");
+ Log::warning("Could not parse flight number for auto-update", ['flight_id' => $flight->id, 'flight_number' => $flight->flight_number]);
+ $flight->update(['auto_update' => false]);
+ return;
+ }
+
+ $airlineCode = strtoupper($matches[1]);
+ $flightNumber = $matches[2];
+ $arrivalDate = $flight->arrival_date->setTimezone($flight->arrivalAirport->timezone);
+
+ $data = $this->flightStats->fetchFlightData($airlineCode, $flightNumber, $arrivalDate);
+
+ // Case 2: valid flight number, but the API had nothing for it.
+ if (!$data) {
+ $this->warn("No flight data returned for {$airlineCode}{$flightNumber}");
+ $this->notifyLookupFailed($flight, $airlineCode, $flightNumber);
+ return;
+ }
+
+ // Case 3: API returned data, but for the wrong route — treat like a failed lookup.
+ if ($data->departure_iata !== $flight->departureAirport->iata_code ||
+ $data->arrival_iata !== $flight->arrivalAirport->iata_code) {
+ $this->warn("Airport mismatch for {$airlineCode}{$flightNumber} — API: {$data->departure_iata}→{$data->arrival_iata}, expected: {$flight->departureAirport->iata_code}→{$flight->arrivalAirport->iata_code}");
+ $this->notifyLookupFailed($flight, $airlineCode, $flightNumber);
+ return;
+ }
+
+ // Case 4/5: valid data — apply whatever changed, but always report every field.
+ $this->applyAndNotify($flight, $data, $airlineCode, $flightNumber);
+ }
+
+ protected function notifyLookupFailed(UserFlight $flight, string $airlineCode, string $flightNumber): void
+ {
+ Notification::create([
+ 'user_id' => $flight->user_id,
+ 'title' => "Auto update failed for {$flight->flight_number}",
+ 'body' => "We tried to find up-to-date information for {$airlineCode}{$flightNumber}, but couldn't find a reliable match. Please manually check the aircraft type, registration and departure/arrival times.",
+ 'url' => '/flights/' . $flight->id . '/edit',
+ ]);
+
+ $flight->update(['auto_update' => false]);
+ }
+
+ protected function applyAndNotify(UserFlight $flight, FlightStatData $data, string $airlineCode, string $flightNumber): void
+ {
+ // Snapshot originals before we touch anything, so descriptions reflect the real "from" state.
+ $originalRegistration = $flight->aircraft_registration;
+ $originalDeparture = $flight->departure_date;
+ $originalArrival = $flight->arrival_date;
+ $originalAircraft = $flight->aircraft;
+
+ $updates = [];
+ $aircraftNotFound = false;
+
+ if ($data->aircraft_registration && $data->aircraft_registration !== $originalRegistration) {
+ $updates['aircraft_registration'] = $data->aircraft_registration;
+ }
+
+ if ($data->estimated_departure_utc?->ne($originalDeparture)) {
+ $updates['departure_date'] = $data->estimated_departure_utc;
+ }
+
+ if ($data->estimated_arrival_utc?->ne($originalArrival)) {
+ $updates['arrival_date'] = $data->estimated_arrival_utc;
+ }
+
+ if ($data->equipment_iata && $originalAircraft?->iata_code !== $data->equipment_iata) {
+ $match = $this->flightStats->guessAircraftFromIata($data->equipment_iata);
+
+ if ($match) {
+ $updates['aircraft_id'] = $match->id;
+ } else {
+ $aircraftNotFound = true;
+ Log::warning("No aircraft match for IATA code {$data->equipment_iata} on flight {$airlineCode}{$flightNumber}", [
+ 'flight_id' => $flight->id,
+ ]);
+ }
+ }
+
+ if (!empty($updates)) {
+ $flight->update($updates);
+ $this->info("Updated flight {$airlineCode}{$flightNumber}: " . implode(', ', array_keys($updates)));
+ } else {
+ $this->info("No changes for {$airlineCode}{$flightNumber}");
+ }
+
+ $flight->update(['auto_update' => false]);
+
+ $newAircraft = isset($updates['aircraft_id']) ? Aircraft::find($updates['aircraft_id']) : $originalAircraft;
+
+ $lines = [
+ $this->describeValue(
+ 'Aircraft Registration',
+ $originalRegistration,
+ $data->aircraft_registration,
+ $updates['aircraft_registration'] ?? null,
+ ),
+ $this->describeDate('Departure', $originalDeparture, $data->estimated_departure_utc, $updates['departure_date'] ?? null),
+ $this->describeDate('Arrival', $originalArrival, $data->estimated_arrival_utc, $updates['arrival_date'] ?? null),
+ $this->describeAircraftType($originalAircraft, $newAircraft, $data->equipment_iata, $aircraftNotFound),
+ ];
+
+ $title = $aircraftNotFound
+ ? "Flight {$airlineCode}{$flightNumber} updated — aircraft type not found"
+ : "Flight {$airlineCode}{$flightNumber} updated";
+
+ Notification::create([
+ 'user_id' => $flight->user_id,
+ 'title' => $title,
+ 'body' => implode("\n", $lines),
+ 'url' => '/u/' . $flight->user->name . '/flight/' . $flight->id,
+ ]);
+ }
+
+ /**
+ * Describe a simple string field: from → to, or "no change" / "not found" as appropriate.
+ */
+ protected function describeValue(string $label, ?string $from, ?string $apiValue, ?string $applied): string
+ {
+ $fromLabel = $from ?: 'None';
+
+ if (!$apiValue) {
+ return "{$label}: {$fromLabel} (not returned by API — no change)";
+ }
+
+ if (!$applied) {
+ return "{$label}: {$apiValue} (no change)";
+ }
+
+ return "{$label}: {$fromLabel} → {$apiValue}";
+ }
+
+ protected function describeDate(string $label, ?CarbonInterface $from, ?CarbonInterface $apiValue, mixed $applied): string
+ {
+ $fromLabel = $from?->format('j M Y H:i') . ' ' . ($from?->tzName ?? '') ?: 'Unknown';
+
+ if (!$apiValue) {
+ return "{$label}: {$fromLabel} (not returned by API — no change)";
+ }
+
+ $toLabel = $apiValue->format('j M Y H:i') . ' ' . $apiValue->tzName;
+
+ if (!$applied) {
+ return "{$label}: {$toLabel} (no change)";
+ }
+
+ return "{$label}: {$fromLabel} → {$toLabel}";
+ }
+
+ protected function describeAircraftType(?Aircraft $from, ?Aircraft $to, ?string $equipmentIata, bool $notFound): string
+ {
+ $fromLabel = $from?->display_name_short ?? 'None';
+
+ if (!$equipmentIata) {
+ return "Aircraft Type: {$fromLabel} (not returned by API — no change)";
+ }
+
+ if ($notFound) {
+ return "Aircraft Type: {$fromLabel} (API reported equipment code {$equipmentIata}, but no matching aircraft was found in our database — no change)";
+ }
+
+ if ($to?->id === $from?->id) {
+ return "Aircraft Type: {$fromLabel} (no change)";
+ }
+
+ return "Aircraft Type: {$fromLabel} → {$to?->display_name_short}";
+ }
+}
diff --git a/app/Console/Commands/UpdateArrivedFlights.php b/app/Console/Commands/UpdateArrivedFlights.php
index 9a73a45..b0c6219 100644
--- a/app/Console/Commands/UpdateArrivedFlights.php
+++ b/app/Console/Commands/UpdateArrivedFlights.php
@@ -2,151 +2,32 @@
namespace App\Console\Commands;
-use App\DTOs\FlightStatData;
-use App\Models\Aircraft;
-use App\Models\IataEquipmentCode;
-use App\Models\Notification;
+use App\Enums\FlightNotificationType;
use App\Models\UserFlight;
-use App\Services\FlightStatsService;
-use Carbon\Carbon;
-use Carbon\CarbonImmutable;
+use App\Services\FollowerNotificationService;
use Illuminate\Console\Attributes\Description;
use Illuminate\Console\Attributes\Signature;
use Illuminate\Console\Command;
-use Illuminate\Support\Facades\Http;
-use Illuminate\Support\Facades\Log;
#[Signature('app:update-arrived-flights')]
-#[Description('Command description')]
+#[Description('When a flight arrives, update the arrival_processed_at timestamp and notify followers')]
class UpdateArrivedFlights extends Command
{
-
- public function __construct(protected FlightStatsService $flightStats)
- {
- parent::__construct();
- }
-
- protected function notifyDataError(UserFlight $flight): void
- {
- Notification::create([
- 'user_id' => $flight->user_id,
- 'title' => "Auto update failed for {$flight->flight_number}",
- 'body' => "There was an error fetching flight data for {$flight->flight_number}. Please manually check the aircraft type, registration and departure/arrival times.",
- 'url' => '/flights/' . $flight->id . '/edit'
- ]);
-
- $flight->update(['auto_update' => false]);
- }
-
- /**
- * Execute the console command.
- */
public function handle(): void
{
$now = now()->utc();
- $userFlights = UserFlight::where('arrival_date', '<=', $now->copy()->subHour()->toDateTimeString())
- ->where('auto_update', true)
- ->whereNotNull('flight_number')
+ $userFlights = UserFlight::where('arrival_date', '<=', $now->toDateTimeString())
+ ->where(function ($query) {
+ $query->whereNull('arrival_processed_at')
+ ->orWhereColumn('arrival_processed_at', '<', 'arrival_date');
+ })
->get();
- $this->info("Found {$userFlights->count()} flights.");
-
+ echo $userFlights->count() . " flights to update\n";
foreach ($userFlights as $flight) {
- 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}");
- $this->notifyDataError($flight);
- continue;
- }
-
- $airlineCode = strtoupper($matches[1]);
- $flightNumber = $matches[2];
-
- $arrivalDate = $flight->arrival_date->setTimezone($flight->arrivalAirport->timezone);
-
- $data = $this->flightStats->fetchFlightData($airlineCode, $flightNumber, $arrivalDate);
-
- if (!$data) {
- $this->warn("No flight data returned for {$airlineCode}{$flightNumber}");
- $this->notifyDataError($flight);
- continue;
- }
-
- if ($data->departure_iata !== $flight->departureAirport->iata_code ||
- $data->arrival_iata !== $flight->arrivalAirport->iata_code) {
- $this->warn("Airport mismatch for {$airlineCode}{$flightNumber} — API: {$data->departure_iata}→{$data->arrival_iata}, expected: {$flight->departureAirport->iata_code}→{$flight->arrivalAirport->iata_code}");
- $this->notifyDataError($flight);
-
- continue;
- }
-
- $updates = [];
-
- if ($data->aircraft_registration && $data->aircraft_registration !== $flight->aircraft_registration) {
- $updates['aircraft_registration'] = $data->aircraft_registration;
- }
-
- if ($data->estimated_departure_utc?->ne($flight->departure_date)) {
- $updates['departure_date'] = $data->estimated_departure_utc;
- }
-
- if ($data->estimated_arrival_utc?->ne($flight->arrival_date)) {
- $updates['arrival_date'] = $data->estimated_arrival_utc;
- }
-
- if ($data->equipment_iata) {
- $currentAircraft = $flight->aircraft;
-
- if ($currentAircraft?->iata_code !== $data->equipment_iata) {
- $match = $this->flightStats->guessAircraftFromIata($data->equipment_iata);
-
- if ($match) {
- $updates['aircraft_id'] = $match->id;
- } else {
- Log::info("No aircraft match for IATA code {$data->equipment_iata} on flight {$airlineCode}{$flightNumber}");
- }
- }
- }
-
- if (!empty($updates)) {
- $flight->update($updates);
- $this->info("Updated flight {$airlineCode}{$flightNumber}: " . implode(', ', array_keys($updates)));
-
- $changeDescriptions = [];
- if (isset($updates['aircraft_registration'])) {
- $changeDescriptions[] = "Registration updated to {$updates['aircraft_registration']}";
- }
- if (isset($updates['departure_date'])) {
- $changeDescriptions[] = "Departure updated to {$updates['departure_date']}";
- }
- if (isset($updates['arrival_date'])) {
- $changeDescriptions[] = "Arrival updated to {$updates['arrival_date']}";
- }
- if (isset($updates['aircraft_id'])) {
- $aircraft = Aircraft::find($updates['aircraft_id']);
- $changeDescriptions[] = "Aircraft type updated to {$aircraft->display_name_short}";
- }
-
- Notification::create([
- 'user_id' => $flight->user_id,
- 'title' => "Flight {$airlineCode}{$flightNumber} updated",
- 'body' => implode("\n", $changeDescriptions),
- 'url' => '/u/'. $flight->user->name . '/flight/'. $flight->id,
- ]);
- } else {
- $this->info("No changes for {$airlineCode}{$flightNumber}");
-
- Notification::create([
- 'user_id' => $flight->user_id,
- 'title' => "Flight {$airlineCode}{$flightNumber} updated — no changes",
- 'body' => "Your flight was completed and no updates were made to aircraft, registration, or departure/arrival times.",
- 'url' => '/u/'. $flight->user->name . '/flight/'. $flight->id,
- ]);
- }
-
- $flight->update(['auto_update' => false]);
+ $flight->update(['arrival_processed_at' => $now]);
+ new FollowerNotificationService()->notifyFlightEvent($flight, FlightNotificationType::Arrived);
}
}
}
diff --git a/app/Console/Commands/UpdateDepartedFlights.php b/app/Console/Commands/UpdateDepartedFlights.php
index 4ff323d..b0ffa15 100644
--- a/app/Console/Commands/UpdateDepartedFlights.php
+++ b/app/Console/Commands/UpdateDepartedFlights.php
@@ -2,22 +2,18 @@
namespace App\Console\Commands;
-use App\Models\Aircraft;
-use App\Models\Notification;
-use App\Models\User;
+use App\Enums\FlightNotificationType;
use App\Models\UserFlight;
+use App\Services\FollowerNotificationService;
use Illuminate\Console\Attributes\Description;
use Illuminate\Console\Attributes\Signature;
use Illuminate\Console\Command;
-use Illuminate\Support\Facades\Log;
#[Signature('app:update-departed-flights')]
#[Description('Command description')]
class UpdateDepartedFlights extends Command
{
- /**
- * Execute the console command.
- */
+
public function handle(): void
{
$now = now()->utc();
@@ -32,33 +28,7 @@ class UpdateDepartedFlights extends Command
foreach ($userFlights as $flight) {
$flight->update(['departure_processed_at' => $now]);
- $this->notifyFollowersOnDeparture($flight);
+ new FollowerNotificationService()->notifyFlightEvent($flight, FlightNotificationType::Departed);
}
}
-
- protected function notifyFollowersOnDeparture(UserFlight $flight): void
- {
- $user = $flight->user;
- $title = "{$user->name} is taking off!";
- $flightNumber = $flight->flight_number ? "On {$flight->flight_number} from" : 'From';
- $body = "{$flightNumber} {$flight->departureAirport->municipality} → {$flight->arrivalAirport->municipality}";
- $url = route('profile.flight', [
- 'user' => $user->id,
- 'userFlight' => $flight->id,
- ]);
-
- $user->followers()->get()->each(function (User $follower) use ($title, $body, $url) {
- $notify = $follower->getSetting('notify_on_flight_departure');
-
- if ($notify) {
- Notification::create([
- 'user_id' => $follower->id,
- 'title' => $title,
- 'body' => $body,
- 'url' => $url,
- ]);
- }
-
- });
- }
}
diff --git a/app/Enums/FlightNotificationType.php b/app/Enums/FlightNotificationType.php
new file mode 100644
index 0000000..ffe21d5
--- /dev/null
+++ b/app/Enums/FlightNotificationType.php
@@ -0,0 +1,63 @@
+ 'notify_on_upcoming_flight_added',
+ self::Logged => 'notify_on_historic_flight_added',
+ self::Cancelled => 'notify_on_flight_cancellation',
+ self::Departed => 'notify_on_flight_departure',
+ self::Arrived => 'notify_on_flight_arrival',
+ };
+ }
+
+ public function title(User $user): string
+ {
+ return match ($this) {
+ self::Booked => "{$user->name} booked a new flight",
+ self::Logged => "{$user->name} logged a new flight",
+ self::Cancelled => "{$user->name} cancelled a flight",
+ self::Departed => "{$user->name} is taking off!",
+ self::Arrived => "{$user->name} has landed!",
+ };
+ }
+
+ public function body(UserFlight $flight): string
+ {
+ $flightNumber = $flight->flight_number ? "{$flight->flight_number} from" : 'From';
+
+ return match ($this) {
+ self::Departed => "{$flightNumber} {$flight->departureAirport->municipality} → {$flight->arrivalAirport->municipality}",
+ self::Arrived => "In {$flight->arrivalAirport->municipality}",
+ default => "{$flightNumber} {$flight->departureAirport->municipality} ({$flight->departureAirport->display_code}) → "
+ . "{$flight->arrivalAirport->municipality} ({$flight->arrivalAirport->display_code}) on {$flight->departure_date_display}",
+ };
+ }
+
+ public function url(UserFlight $flight): string
+ {
+ return match ($this) {
+ self::Cancelled => route('profile.view', [$flight->user->name]),
+ self::Departed, self::Arrived => route('profile.flight', ['user' => $flight->user->id, 'userFlight' => $flight->id]),
+ default => route('profile.departure-board', [$flight->user->name, $flight->id]),
+ };
+ }
+
+ public static function forCreation(UserFlight $flight): self
+ {
+ return $flight->departure_date->isFuture() ? self::Booked : self::Logged;
+ }
+}
diff --git a/app/Http/Controllers/Api/UserApiController.php b/app/Http/Controllers/Api/UserApiController.php
index 4f7dfa4..8a20106 100644
--- a/app/Http/Controllers/Api/UserApiController.php
+++ b/app/Http/Controllers/Api/UserApiController.php
@@ -3,12 +3,10 @@
namespace App\Http\Controllers\Api;
use App\Http\Controllers\ApiController;
-use App\Http\Controllers\UserFlightController;
use App\Models\User;
use App\Models\UserFlight;
use Carbon\Carbon;
use Illuminate\Http\JsonResponse;
-use Illuminate\Http\Request;
use Illuminate\Support\Facades\Gate;
class UserApiController extends ApiController
diff --git a/app/Http/Controllers/FlightController.php b/app/Http/Controllers/FlightController.php
index d0d4a77..6bcf171 100644
--- a/app/Http/Controllers/FlightController.php
+++ b/app/Http/Controllers/FlightController.php
@@ -232,8 +232,6 @@ class FlightController extends Controller
];
}
-
-
public function store(Request $request)
{
$validated = $request->validate($this->rules());
@@ -248,42 +246,10 @@ class FlightController extends Controller
],
]);
- $this->notifyFollowersOnCreation($newFlight);
return redirect()->route('profile.departure-board', [Auth::user()->name, $newFlight->id]);
}
- protected function notifyFollowersOnCreation(UserFlight $flight): void
- {
- $user = $flight->user;
- $isFuture = $flight->departure_date->isFuture();
-
- $title = $isFuture
- ? "{$user->name} booked a new flight"
- : "{$user->name} logged a new flight";
-
-
- $flightNumber = $flight->flight_number ? "{$flight->flight_number} from" : 'From';
- $body = "{$flightNumber} {$flight->departureAirport->municipality} ({$flight->departureAirport->display_code}) → {$flight->arrivalAirport->municipality} ({$flight->arrivalAirport->display_code}) on {$flight->departure_date_display}";
- $url = route('profile.departure-board', [$user->name, $flight->id]);
-
- $user->followers()->get()->each(function (User $follower) use ($title, $body, $url, $isFuture) {
- $notifyForFuture = $follower->getSetting('notify_on_upcoming_flight_added');
- $notifyForHistoric = $follower->getSetting('notify_on_historic_flight_added');
-
- if (($isFuture && $notifyForFuture) || (!$isFuture && $notifyForHistoric)) {
- Notification::create([
- 'user_id' => $follower->id,
- 'title' => $title,
- 'body' => $body,
- 'url' => $url,
- ]);
- }
-
- }
- );
- }
-
public function update(Request $request, UserFlight $flight)
{
@@ -323,36 +289,11 @@ class FlightController extends Controller
],
]);
- if ($isFuture) {
- $this->notifyFollowersOnCancellation($flight);
- }
-
$flight->delete();
return redirect()->route('profile.'.$referrer, [Auth::user()->name]);
}
- protected function notifyFollowersOnCancellation(UserFlight $flight): void
- {
- $user = $flight->user;
-
- $title = "{$user->name} cancelled a flight";
- $flightNumber = $flight->flight_number ? "{$flight->flight_number} from" : 'From';
- $body = "{$flightNumber} {$flight->departureAirport->municipality} ({$flight->departureAirport->display_code}) → {$flight->arrivalAirport->municipality} ({$flight->arrivalAirport->display_code}) on {$flight->departure_date_display}";
- $url = route('profile.view', [$user->name]);
-
- $user->followers()->get()->each(function (User $follower) use ($title, $body, $url) {
- if ($follower->getSetting('notify_on_flight_cancellation')) {
- Notification::create([
- 'user_id' => $follower->id,
- 'title' => $title,
- 'body' => $body,
- 'url' => $url,
- ]);
- }
- });
- }
-
public function staticData() : array {
return [
'seat_types' => SeatType::orderBy('id')->get()->toArray(),
diff --git a/app/Http/Controllers/FlightImportController.php b/app/Http/Controllers/FlightImportController.php
index ab9f0fc..1db5cac 100644
--- a/app/Http/Controllers/FlightImportController.php
+++ b/app/Http/Controllers/FlightImportController.php
@@ -271,44 +271,11 @@ class FlightImportController extends Controller
],
]);
- $this->notifyFollowersOnCreation($newFlight);
ImportedFlight::destroy($validated['imported_flight_id']);
return to_route('reconcile');
}
- protected function notifyFollowersOnCreation(UserFlight $flight): void
- {
- $user = $flight->user;
- $isFuture = $flight->departure_date->isFuture();
-
- $title = $isFuture
- ? "{$user->name} booked a new flight"
- : "{$user->name} logged a new flight";
-
-
- $flightNumber = $flight->flight_number ? "{$flight->flight_number} from" : 'From';
- $body = "{$flightNumber} {$flight->departureAirport->municipality} ({$flight->departureAirport->display_code}) → {$flight->arrivalAirport->municipality} ({$flight->arrivalAirport->display_code}) on {$flight->departure_date_display}";
- $url = route('profile.departure-board', [$user->name, $flight->id]);
-
- $user->followers()->get()->each(function (User $follower) use ($title, $body, $url, $isFuture) {
- $notifyForFuture = $follower->getSetting('notify_on_upcoming_flight_added');
- $notifyForHistoric = $follower->getSetting('notify_on_historic_flight_added');
-
- if (($isFuture && $notifyForFuture) || (!$isFuture && $notifyForHistoric)) {
- Notification::create([
- 'user_id' => $follower->id,
- 'title' => $title,
- 'body' => $body,
- 'url' => $url,
- ]);
- }
-
- }
- );
- }
-
-
private function validateCsvFormat(string $path): ?string
{
$handle = fopen($path, 'r');
diff --git a/app/Http/Controllers/FollowerController.php b/app/Http/Controllers/FollowerController.php
index 9d063cf..3910524 100644
--- a/app/Http/Controllers/FollowerController.php
+++ b/app/Http/Controllers/FollowerController.php
@@ -67,7 +67,7 @@ class FollowerController extends Controller
? auth()->user()->name . ' is now following you.'
: auth()->user()->name . ' wants to follow you.',
'is_achievement' => false,
- 'url' => $canView ? '/u/' . auth()->user()->name : '/follow-requests',
+ 'url' => $canView ? '/u/' . auth()->user()->name : '/settings/followers',
]);
}
diff --git a/app/Models/Guest.php b/app/Models/Guest.php
deleted file mode 100644
index 00afd48..0000000
--- a/app/Models/Guest.php
+++ /dev/null
@@ -1,11 +0,0 @@
- 'immutable_datetime',
'auto_update' => 'boolean',
'departure_processed_at' => 'datetime',
+ 'arrival_processed_at' => 'datetime',
];
protected $appends = [
diff --git a/app/Observers/FlightObserver.php b/app/Observers/FlightObserver.php
index ddfed4b..b107eb2 100644
--- a/app/Observers/FlightObserver.php
+++ b/app/Observers/FlightObserver.php
@@ -2,12 +2,15 @@
namespace App\Observers;
+use App\Enums\FlightNotificationType;
use App\Models\User;
use App\Models\UserFlight;
+use App\Services\FollowerNotificationService;
use Illuminate\Support\Facades\Cache;
class FlightObserver
{
+
protected function clearCache(UserFlight $flight): void
{
Cache::forget("user_flights_{$flight->user->id}");
@@ -27,6 +30,7 @@ class FlightObserver
{
$flight->user->calculateAchievements();
$this->clearCache($flight);
+ new FollowerNotificationService()->notifyFlightEvent($flight, FlightNotificationType::forCreation($flight));
}
/**
@@ -49,5 +53,9 @@ class FlightObserver
{
$flight->user->calculateAchievements();
$this->clearCache($flight);
+
+ if ($flight->departure_date->isFuture()) {
+ new FollowerNotificationService()->notifyFlightEvent($flight, FlightNotificationType::Cancelled);
+ }
}
}
diff --git a/app/Services/FollowerNotificationService.php b/app/Services/FollowerNotificationService.php
new file mode 100644
index 0000000..adbea34
--- /dev/null
+++ b/app/Services/FollowerNotificationService.php
@@ -0,0 +1,42 @@
+notify(
+ user: $flight->user,
+ settingKey: $type->settingKey(),
+ title: $type->title($flight->user),
+ body: $type->body($flight),
+ url: $type->url($flight),
+ );
+ }
+
+ /**
+ * Generic entry point for any future "notify all followers about X" need
+ * that isn't tied to a flight event.
+ */
+ public function notify(User $user, string $settingKey, string $title, string $body, string $url): void
+ {
+ $user->followers()
+ ->get()
+ ->filter(fn (User $follower) => $follower->getSetting($settingKey))
+ ->each(fn (User $follower) => Notification::create([
+ 'user_id' => $follower->id,
+ 'title' => $title,
+ 'body' => $body,
+ 'url' => $url,
+ ]));
+ }
+}
diff --git a/app/Settings/SettingsRegistry.php b/app/Settings/SettingsRegistry.php
index 7367795..fd995d3 100644
--- a/app/Settings/SettingsRegistry.php
+++ b/app/Settings/SettingsRegistry.php
@@ -145,6 +145,13 @@ class SettingsRegistry
'label' => 'Notify Me When Someone I Follow Logs an Upcoming Flight',
'default' => true,
],
+ [
+ 'category' => 'Notifications',
+ 'key' => 'notify_on_flight_cancellation',
+ 'type' => 'checkbox',
+ 'label' => 'Notify Me When Someone I Follow Cancels an Upcoming Flight',
+ 'default' => false,
+ ],
[
'category' => 'Notifications',
'key' => 'notify_on_flight_departure',
@@ -152,19 +159,12 @@ class SettingsRegistry
'label' => 'Notify Me When Someone I Follow Departs',
'default' => true,
],
-/* [
+ [
'category' => 'Notifications',
'key' => 'notify_on_flight_arrival',
'type' => 'checkbox',
'label' => 'Notify Me When Someone I Follow Arrives',
'default' => true,
- ],*/
- [
- 'category' => 'Notifications',
- 'key' => 'notify_on_flight_cancellation',
- 'type' => 'checkbox',
- 'label' => 'Notify Me When Someone I Follow Cancels an Upcoming Flight',
- 'default' => false,
],
];
}
diff --git a/database/migrations/2026_07_02_121810_add_arrival_processing_column_to_user_flights.php b/database/migrations/2026_07_02_121810_add_arrival_processing_column_to_user_flights.php
new file mode 100644
index 0000000..93b6b0c
--- /dev/null
+++ b/database/migrations/2026_07_02_121810_add_arrival_processing_column_to_user_flights.php
@@ -0,0 +1,30 @@
+timestamp('arrival_processed_at')->nullable()->after('departure_processed_at');
+ });
+
+ DB::table('user_flights')
+ ->where('arrival_date', '<=', now()->utc())
+ ->whereNull('arrival_processed_at')
+ ->update(['arrival_processed_at' => now()->utc()]);
+ }
+
+ /**
+ * Reverse the migrations.
+ */
+ public function down(): void
+ {
+ }
+};
diff --git a/resources/js/Pages/Auth/Login.vue b/resources/js/Pages/Auth/Login.vue
index b1bc2cb..532673d 100644
--- a/resources/js/Pages/Auth/Login.vue
+++ b/resources/js/Pages/Auth/Login.vue
@@ -10,10 +10,17 @@ defineProps<{
status?: string;
}>();
+const isStandalone = (): boolean => {
+ if (typeof window === 'undefined') return false;
+
+ return window.matchMedia('(display-mode: standalone)').matches
+ || (window.navigator as any).standalone === true;
+};
+
const form = useForm({
email: '',
password: '',
- remember: false,
+ remember: isStandalone(),
});
const submit = () => {
diff --git a/resources/js/Pages/Welcome.vue b/resources/js/Pages/Welcome.vue
deleted file mode 100644
index 63eed9c..0000000
--- a/resources/js/Pages/Welcome.vue
+++ /dev/null
@@ -1,386 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
- Dashboard
-
-
-
-
- Log in
-
-
-
- Register
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Documentation
-
-
-
- Laravel has wonderful documentation
- covering every aspect of the
- framework. Whether you are a
- newcomer or have prior experience
- with Laravel, we recommend reading
- our documentation from beginning to
- end.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Laracasts
-
-
-
- Laracasts offers thousands of video
- tutorials on Laravel, PHP, and JavaScript
- development. Check them out, see for
- yourself, and massively level up your
- development skills in the process.
-
-
-
-
-
-
-
-
-
-
-
-
-
- Laravel News
-
-
-
- Laravel News is a community driven portal
- and newsletter aggregating all of the latest
- and most important news in the Laravel
- ecosystem, including new package releases
- and tutorials.
-
-
-
-
-
-
-
-
-
-
-
-
-
- Vibrant Ecosystem
-
-
-
- Laravel's robust library of first-party
- tools and libraries, such as
- Forge ,
- Vapor ,
- Nova ,
- Envoyer , and
- Herd
- help you take your projects to the next
- level. Pair them with powerful open source
- libraries like
- Cashier ,
- Dusk ,
- Echo ,
- Horizon ,
- Sanctum ,
- Telescope , and more.
-
-
-
-
-
-
-
- Laravel v{{ laravelVersion }} (PHP v{{ phpVersion }})
-
-
-
-
-
diff --git a/routes/console.php b/routes/console.php
index 3855bd1..c6f88c7 100644
--- a/routes/console.php
+++ b/routes/console.php
@@ -4,6 +4,7 @@ use Illuminate\Foundation\Inspiring;
use Illuminate\Support\Facades\Artisan;
Schedule::command('app:update-alliances')->dailyAt('09:00')->runInBackground();
-Schedule::command('app:update-arrived-flights')->hourly()->runInBackground();
Schedule::command('app:update-departed-flights')->everyMinute()->runInBackground();
+Schedule::command('app:update-arrived-flights')->everyMinute()->runInBackground();
+Schedule::command('app:get-aircraft-info-for-arrived-flights')->hourly()->runInBackground();
Schedule::command('app:flight-feed-update')->everyMinute()->runInBackground();