diff --git a/app/Livewire/Admin/Maintenance.php b/app/Livewire/Admin/Maintenance.php index 8a9f27a..3ebd579 100644 --- a/app/Livewire/Admin/Maintenance.php +++ b/app/Livewire/Admin/Maintenance.php @@ -124,6 +124,22 @@ class Maintenance extends Component $this->dispatch('notify', message: __('maintenance.published')); } + /** + * Re-run announcements for a published window. Idempotent (ledger-guarded), + * so it only fills gaps — the retry path when a transient queue outage left + * some affected customers un-notified. + */ + public function resend(string $uuid): void + { + $this->authorize('maintenance.manage'); + $window = MaintenanceWindow::query()->where('uuid', $uuid)->first(); + if ($window === null || $window->state !== 'scheduled') { + return; + } + $this->sendAnnouncements($window); + $this->dispatch('notify', message: __('maintenance.notified')); + } + public function cancel(string $uuid): void { $this->authorize('maintenance.manage'); @@ -176,6 +192,7 @@ class Maintenance extends Component 'hosts' => $w->hosts_count, 'affected' => $w->affectedCustomers()->count(), 'is_draft' => $w->state === 'draft', + 'notifiable' => $w->state === 'scheduled' && in_array($w->derivedState(), ['upcoming', 'active'], true), 'cancellable' => in_array($w->derivedState(), ['draft', 'upcoming', 'active'], true), ]); diff --git a/app/Livewire/Admin/Settings.php b/app/Livewire/Admin/Settings.php index 7f9835e..d1ee108 100644 --- a/app/Livewire/Admin/Settings.php +++ b/app/Livewire/Admin/Settings.php @@ -7,6 +7,7 @@ use App\Models\User; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Hash; use Illuminate\Support\Str; +use Spatie\Permission\Models\Role; use Livewire\Attributes\Layout; use Livewire\Attributes\Validate; use Livewire\Component; @@ -105,6 +106,10 @@ class Settings extends Component } $result = DB::transaction(function () use ($id, $role) { + // Serialize on the Owner role so the global owner count can't be + // raced to zero by concurrent demotions of different owners. + Role::query()->where('name', 'Owner')->lockForUpdate()->first(); + $target = User::query()->whereKey($id)->lockForUpdate()->first(); if ($target === null) { return 'gone'; @@ -138,6 +143,8 @@ class Settings extends Component $this->authorize('staff.manage'); $result = DB::transaction(function () use ($id) { + Role::query()->where('name', 'Owner')->lockForUpdate()->first(); + $target = User::query()->whereKey($id)->lockForUpdate()->first(); if ($target === null || ! $target->isOperator()) { return 'gone'; diff --git a/lang/de/maintenance.php b/lang/de/maintenance.php index e5e2f93..757cbdd 100644 --- a/lang/de/maintenance.php +++ b/lang/de/maintenance.php @@ -31,6 +31,8 @@ return [ 'cancel' => 'Stornieren', 'mail_note' => 'Beim Veröffentlichen erhalten betroffene Kunden eine Ankündigungs-E-Mail (aktuell Log/Mock).', + 'notify_again' => 'Erneut benachrichtigen', + 'notified' => 'Benachrichtigungen versendet.', 'draft_saved' => 'Entwurf gespeichert.', 'published' => 'Wartung veröffentlicht — Kunden werden benachrichtigt.', 'cancelled' => 'Wartung storniert.', diff --git a/lang/en/maintenance.php b/lang/en/maintenance.php index 66b8e4b..dc38eee 100644 --- a/lang/en/maintenance.php +++ b/lang/en/maintenance.php @@ -31,6 +31,8 @@ return [ 'cancel' => 'Cancel', 'mail_note' => 'On publish, affected customers receive an announcement email (currently log/mock).', + 'notify_again' => 'Notify again', + 'notified' => 'Notifications sent.', 'draft_saved' => 'Draft saved.', 'published' => 'Maintenance published — customers are being notified.', 'cancelled' => 'Maintenance cancelled.', diff --git a/resources/views/livewire/admin/maintenance.blade.php b/resources/views/livewire/admin/maintenance.blade.php index 037a4db..12c5604 100644 --- a/resources/views/livewire/admin/maintenance.blade.php +++ b/resources/views/livewire/admin/maintenance.blade.php @@ -34,6 +34,9 @@ @if ($w['is_draft']) @endif + @if ($w['notifiable']) + + @endif @if ($w['cancellable']) @endif