fix(admin): retry path for missed maintenance emails; lock Owner role to serialize last-owner guard
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>feat/portal-design
parent
bc2e95ba18
commit
6593dae946
|
|
@ -124,6 +124,22 @@ class Maintenance extends Component
|
||||||
$this->dispatch('notify', message: __('maintenance.published'));
|
$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
|
public function cancel(string $uuid): void
|
||||||
{
|
{
|
||||||
$this->authorize('maintenance.manage');
|
$this->authorize('maintenance.manage');
|
||||||
|
|
@ -176,6 +192,7 @@ class Maintenance extends Component
|
||||||
'hosts' => $w->hosts_count,
|
'hosts' => $w->hosts_count,
|
||||||
'affected' => $w->affectedCustomers()->count(),
|
'affected' => $w->affectedCustomers()->count(),
|
||||||
'is_draft' => $w->state === 'draft',
|
'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),
|
'cancellable' => in_array($w->derivedState(), ['draft', 'upcoming', 'active'], true),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ use App\Models\User;
|
||||||
use Illuminate\Support\Facades\DB;
|
use Illuminate\Support\Facades\DB;
|
||||||
use Illuminate\Support\Facades\Hash;
|
use Illuminate\Support\Facades\Hash;
|
||||||
use Illuminate\Support\Str;
|
use Illuminate\Support\Str;
|
||||||
|
use Spatie\Permission\Models\Role;
|
||||||
use Livewire\Attributes\Layout;
|
use Livewire\Attributes\Layout;
|
||||||
use Livewire\Attributes\Validate;
|
use Livewire\Attributes\Validate;
|
||||||
use Livewire\Component;
|
use Livewire\Component;
|
||||||
|
|
@ -105,6 +106,10 @@ class Settings extends Component
|
||||||
}
|
}
|
||||||
|
|
||||||
$result = DB::transaction(function () use ($id, $role) {
|
$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();
|
$target = User::query()->whereKey($id)->lockForUpdate()->first();
|
||||||
if ($target === null) {
|
if ($target === null) {
|
||||||
return 'gone';
|
return 'gone';
|
||||||
|
|
@ -138,6 +143,8 @@ class Settings extends Component
|
||||||
$this->authorize('staff.manage');
|
$this->authorize('staff.manage');
|
||||||
|
|
||||||
$result = DB::transaction(function () use ($id) {
|
$result = DB::transaction(function () use ($id) {
|
||||||
|
Role::query()->where('name', 'Owner')->lockForUpdate()->first();
|
||||||
|
|
||||||
$target = User::query()->whereKey($id)->lockForUpdate()->first();
|
$target = User::query()->whereKey($id)->lockForUpdate()->first();
|
||||||
if ($target === null || ! $target->isOperator()) {
|
if ($target === null || ! $target->isOperator()) {
|
||||||
return 'gone';
|
return 'gone';
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,8 @@ return [
|
||||||
'cancel' => 'Stornieren',
|
'cancel' => 'Stornieren',
|
||||||
'mail_note' => 'Beim Veröffentlichen erhalten betroffene Kunden eine Ankündigungs-E-Mail (aktuell Log/Mock).',
|
'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.',
|
'draft_saved' => 'Entwurf gespeichert.',
|
||||||
'published' => 'Wartung veröffentlicht — Kunden werden benachrichtigt.',
|
'published' => 'Wartung veröffentlicht — Kunden werden benachrichtigt.',
|
||||||
'cancelled' => 'Wartung storniert.',
|
'cancelled' => 'Wartung storniert.',
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,8 @@ return [
|
||||||
'cancel' => 'Cancel',
|
'cancel' => 'Cancel',
|
||||||
'mail_note' => 'On publish, affected customers receive an announcement email (currently log/mock).',
|
'mail_note' => 'On publish, affected customers receive an announcement email (currently log/mock).',
|
||||||
|
|
||||||
|
'notify_again' => 'Notify again',
|
||||||
|
'notified' => 'Notifications sent.',
|
||||||
'draft_saved' => 'Draft saved.',
|
'draft_saved' => 'Draft saved.',
|
||||||
'published' => 'Maintenance published — customers are being notified.',
|
'published' => 'Maintenance published — customers are being notified.',
|
||||||
'cancelled' => 'Maintenance cancelled.',
|
'cancelled' => 'Maintenance cancelled.',
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,9 @@
|
||||||
@if ($w['is_draft'])
|
@if ($w['is_draft'])
|
||||||
<button type="button" wire:click="publishExisting('{{ $w['uuid'] }}')" class="rounded-md border border-line px-2.5 py-1.5 text-xs font-semibold text-body hover:border-accent-border hover:text-accent-text">{{ __('maintenance.publish') }}</button>
|
<button type="button" wire:click="publishExisting('{{ $w['uuid'] }}')" class="rounded-md border border-line px-2.5 py-1.5 text-xs font-semibold text-body hover:border-accent-border hover:text-accent-text">{{ __('maintenance.publish') }}</button>
|
||||||
@endif
|
@endif
|
||||||
|
@if ($w['notifiable'])
|
||||||
|
<button type="button" wire:click="resend('{{ $w['uuid'] }}')" class="rounded-md border border-line px-2.5 py-1.5 text-xs font-semibold text-body hover:border-accent-border hover:text-accent-text">{{ __('maintenance.notify_again') }}</button>
|
||||||
|
@endif
|
||||||
@if ($w['cancellable'])
|
@if ($w['cancellable'])
|
||||||
<button type="button" wire:click="cancel('{{ $w['uuid'] }}')" class="rounded-md border border-line px-2.5 py-1.5 text-xs font-semibold text-muted hover:border-danger hover:text-danger">{{ __('maintenance.cancel') }}</button>
|
<button type="button" wire:click="cancel('{{ $w['uuid'] }}')" class="rounded-md border border-line px-2.5 py-1.5 text-xs font-semibold text-muted hover:border-danger hover:text-danger">{{ __('maintenance.cancel') }}</button>
|
||||||
@endif
|
@endif
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue