Add guest push notifications and queue alerts

This commit is contained in:
Codex Agent
2025-11-12 20:38:49 +01:00
parent 2c412e3764
commit 574aa47ce7
34 changed files with 1806 additions and 74 deletions

View File

@@ -3,7 +3,11 @@
namespace App\Console\Commands;
use App\Console\Concerns\InteractsWithCacheLocks;
use App\Enums\GuestNotificationAudience;
use App\Enums\GuestNotificationType;
use App\Models\Event;
use App\Models\EventMediaAsset;
use App\Services\GuestNotificationService;
use Illuminate\Console\Command;
use Illuminate\Contracts\Cache\Lock;
use Illuminate\Queue\QueueManager;
@@ -20,6 +24,11 @@ class CheckUploadQueuesCommand extends Command
protected $description = 'Inspect upload-related queues and flag stalled or overloaded workers.';
public function __construct(private readonly GuestNotificationService $guestNotifications)
{
parent::__construct();
}
public function handle(QueueManager $queueManager): int
{
$lockSeconds = (int) config('storage-monitor.queue_health.lock_seconds', 120);
@@ -117,6 +126,8 @@ class CheckUploadQueuesCommand extends Command
count($alerts)
));
$this->maybeNotifyGuests($alerts);
return self::SUCCESS;
} finally {
if ($lock instanceof Lock) {
@@ -125,6 +136,130 @@ class CheckUploadQueuesCommand extends Command
}
}
private function maybeNotifyGuests(array $alerts): void
{
if (empty($alerts)) {
return;
}
$this->dispatchPendingAlerts();
$this->dispatchFailureAlerts();
}
private function dispatchPendingAlerts(): void
{
$threshold = max(1, (int) config('storage-monitor.queue_health.pending_event_threshold', 5));
$minutes = max(1, (int) config('storage-monitor.queue_health.pending_event_minutes', 8));
$pending = EventMediaAsset::query()
->selectRaw('event_id, COUNT(*) as pending_count, MIN(created_at) as oldest_created_at')
->where('status', 'pending')
->where('created_at', '<=', now()->subMinutes($minutes))
->groupBy('event_id')
->havingRaw('COUNT(*) >= ?', [$threshold])
->limit(50)
->get();
foreach ($pending as $row) {
$event = Event::query()->find($row->event_id);
if (! $event) {
continue;
}
$title = 'Uploads werden noch verarbeitet …';
if ($this->recentlySentAlert($event->id, $title)) {
continue;
}
$count = (int) $row->pending_count;
$body = $count > 1
? sprintf('%d Fotos stehen noch in der Warteschlange. Wir sagen Bescheid, sobald alles gespeichert ist.', $count)
: 'Ein Upload-Schub wird gerade verarbeitet. Danke für deine Geduld!';
$this->guestNotifications->createNotification(
$event,
GuestNotificationType::UPLOAD_ALERT,
$title,
$body,
[
'audience_scope' => GuestNotificationAudience::ALL,
'priority' => 1,
'expires_at' => now()->addMinutes(90),
]
);
$this->rememberAlert($event->id, $title);
}
}
private function dispatchFailureAlerts(): void
{
$threshold = max(1, (int) config('storage-monitor.queue_health.failed_event_threshold', 2));
$minutes = max(1, (int) config('storage-monitor.queue_health.failed_event_minutes', 30));
$failed = EventMediaAsset::query()
->selectRaw('event_id, COUNT(*) as failed_count')
->where('status', 'failed')
->where('updated_at', '>=', now()->subMinutes($minutes))
->groupBy('event_id')
->havingRaw('COUNT(*) >= ?', [$threshold])
->limit(50)
->get();
foreach ($failed as $row) {
$event = Event::query()->find($row->event_id);
if (! $event) {
continue;
}
$title = 'Einige Uploads mussten neu gestartet werden';
if ($this->recentlySentAlert($event->id, $title)) {
continue;
}
$count = (int) $row->failed_count;
$body = $count > 1
? sprintf('%d Fotos wurden automatisch erneut angestoßen. Bitte öffne kurz die App, falls deine Uploads hängen.', $count)
: 'Ein Upload wurde neu gestartet. Öffne bitte kurz die App, damit nichts verloren geht.';
$this->guestNotifications->createNotification(
$event,
GuestNotificationType::SUPPORT_TIP,
$title,
$body,
[
'audience_scope' => GuestNotificationAudience::ALL,
'priority' => 2,
'expires_at' => now()->addHours(2),
]
);
$this->rememberAlert($event->id, $title);
}
}
private function recentlySentAlert(int $eventId, string $title): bool
{
$key = $this->alertCacheKey($eventId, $title);
return Cache::has($key);
}
private function rememberAlert(int $eventId, string $title): void
{
$key = $this->alertCacheKey($eventId, $title);
$ttl = max(5, (int) config('storage-monitor.queue_health.guest_alert_ttl', 30));
Cache::put($key, true, now()->addMinutes($ttl));
}
private function alertCacheKey(int $eventId, string $title): string
{
return sprintf('guest-queue-alert:%d:%s', $eventId, sha1($title));
}
private function readQueueSize(QueueManager $manager, ?string $connection, string $queue): int
{
try {