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