Added sockets for notifications

This commit is contained in:
2026-06-30 20:49:36 +10:00
parent e551e202d5
commit 6cadd3d9e6
25 changed files with 1639 additions and 193 deletions
@@ -0,0 +1,152 @@
<?php
namespace App\Console\Commands;
use App\DTOs\FlightStatData;
use App\Models\Aircraft;
use App\Models\IataEquipmentCode;
use App\Models\Notification;
use App\Models\UserFlight;
use App\Services\FlightStatsService;
use Carbon\Carbon;
use Carbon\CarbonImmutable;
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')]
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')
->get();
$this->info("Found {$userFlights->count()} flights.");
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]);
}
}
}
+9 -121
View File
@@ -2,42 +2,18 @@
namespace App\Console\Commands;
use App\DTOs\FlightStatData;
use App\Models\Aircraft;
use App\Models\IataEquipmentCode;
use App\Models\Notification;
use App\Models\UserFlight;
use App\Services\FlightStatsService;
use Carbon\Carbon;
use Carbon\CarbonImmutable;
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-departed-flights')]
#[Description('Command description')]
class UpdateDepartedFlights 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.
*/
@@ -45,108 +21,20 @@ class UpdateDepartedFlights extends Command
{
$now = now()->utc();
$userFlights = UserFlight::where('arrival_date', '<=', $now->copy()->subHour()->toDateTimeString())
->where('auto_update', true)
->whereNotNull('flight_number')
$now = now()->utc();
$userFlights = UserFlight::where('departure_date', '<=', $now->toDateTimeString())
->where('departure_date', '>', $now->copy()->subMinute()->toDateTimeString())
->get();
$this->info("Found {$userFlights->count()} flights.");
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->touch();
}
$this->info("Touching flight {$flight->id}");
$result = $flight->touch();
$this->info("Touch result: " . var_export($result, true));
}
}
+28
View File
@@ -0,0 +1,28 @@
<?php
namespace App\Events;
use App\Models\Notification;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Contracts\Broadcasting\ShouldBroadcastNow;
use Illuminate\Queue\SerializesModels;
class NotificationCreated implements ShouldBroadcast
{
use SerializesModels;
public function __construct(public Notification $notification) {}
public function broadcastOn(): array
{
return [
new PrivateChannel('App.Models.User.' . $this->notification->user_id),
];
}
public function broadcastAs(): string
{
return 'notification.created';
}
}
@@ -2,6 +2,7 @@
namespace App\Http\Controllers;
use App\Events\NotificationCreated;
use App\Models\Followee;
use App\Models\Notification;
use App\Models\User;
+1
View File
@@ -34,6 +34,7 @@ class FlightObserver
*/
public function updated(UserFlight $flight): void
{
\Log::info('Observer fired for flight ' . $flight->id);
$flight->user->calculateAchievements();
$this->clearCache($flight);
}
+14
View File
@@ -0,0 +1,14 @@
<?php
namespace App\Observers;
use App\Events\NotificationCreated;
use App\Models\Notification;
class NotificationObserver
{
public function created(Notification $notification): void
{
\Log::info('Notification created: ' . $notification->id);
broadcast(new NotificationCreated($notification));
}
}
+3
View File
@@ -3,9 +3,11 @@
namespace App\Providers;
use App\Models\Airline;
use App\Models\Notification;
use App\Models\UserFlight;
use App\Observers\AirlineObserver;
use App\Observers\FlightObserver;
use App\Observers\NotificationObserver;
use Illuminate\Support\Facades\Vite;
use Illuminate\Support\ServiceProvider;
use Illuminate\Support\Facades\RateLimiter;
@@ -30,6 +32,7 @@ class AppServiceProvider extends ServiceProvider
Vite::prefetch(concurrency: 3);
UserFlight::observe(FlightObserver::class);
Airline::observe(AirlineObserver::class);
Notification::observe(NotificationObserver::class);
RateLimiter::for('api', function (Request $request) {
return $request->user('sanctum')
? Limit::perMinute(60)->by($request->user('sanctum')->id)
@@ -2,6 +2,7 @@
namespace App\Services\Achievements;
use App\Events\NotificationCreated;
use App\Models\Achievement;
use App\Models\Notification;
use App\Models\User;
@@ -50,6 +51,8 @@ class AchievementService
$this->user = $user;
$this->achievementCache = Achievement::all()->keyBy('internal_name');
// In calculateAchievements (or wherever createAchievementNotification is called from)
\Log::info('calculateAchievements running for user ' . $user->id);
// Load once with every relationship any checker could need
$this->flights = $user->flights()->with([
'airline.alliance',