fix(admin): confirm maintenance delivery via MessageSent (retryable until sent); guard resend/cancel on derived state; fix flaky non-unique host wg_ip
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>feat/portal-design
parent
52fa7a34a2
commit
493b81aadf
|
|
@ -136,7 +136,8 @@ class Maintenance extends Component
|
|||
{
|
||||
$this->authorize('maintenance.manage');
|
||||
$window = MaintenanceWindow::query()->where('uuid', $uuid)->first();
|
||||
if ($window === null || $window->state !== 'scheduled') {
|
||||
// Only announce for a window that has not yet ended (derived state).
|
||||
if ($window === null || ! in_array($window->derivedState(), ['upcoming', 'active'], true)) {
|
||||
return;
|
||||
}
|
||||
$this->sendAnnouncements($window);
|
||||
|
|
@ -147,7 +148,8 @@ class Maintenance extends Component
|
|||
{
|
||||
$this->authorize('maintenance.manage');
|
||||
$window = MaintenanceWindow::query()->where('uuid', $uuid)->first();
|
||||
if ($window === null || $window->state === 'cancelled') {
|
||||
// Cannot cancel what is already cancelled or has already completed.
|
||||
if ($window === null || in_array($window->derivedState(), ['cancelled', 'completed'], true)) {
|
||||
return;
|
||||
}
|
||||
$wasPublished = $window->state === 'scheduled';
|
||||
|
|
@ -186,18 +188,15 @@ class Maintenance extends Component
|
|||
['maintenance_window_id' => $window->id, 'customer_id' => $customer->id, 'event' => $event],
|
||||
['email' => $customer->email],
|
||||
);
|
||||
if (! $delivery->wasRecentlyCreated) {
|
||||
// Delivered rows (sent_at set by the MessageSent listener) are final; a
|
||||
// row that is still unconfirmed (sent_at null) is retryable — re-queue it.
|
||||
if ($delivery->sent_at !== null) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
Mail::to($customer->email)
|
||||
->locale($customer->locale ?: config('app.locale'))
|
||||
->queue($mail);
|
||||
} catch (\Throwable $e) {
|
||||
$delivery->delete();
|
||||
|
||||
throw $e;
|
||||
}
|
||||
$mail->notificationId = $delivery->id;
|
||||
Mail::to($customer->email)
|
||||
->locale($customer->locale ?: config('app.locale'))
|
||||
->queue($mail);
|
||||
}
|
||||
|
||||
public function render()
|
||||
|
|
|
|||
|
|
@ -9,17 +9,26 @@ use Illuminate\Contracts\Queue\ShouldQueue;
|
|||
use Illuminate\Mail\Mailable;
|
||||
use Illuminate\Mail\Mailables\Content;
|
||||
use Illuminate\Mail\Mailables\Envelope;
|
||||
use Illuminate\Mail\Mailables\Headers;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class MaintenanceAnnouncementMail extends Mailable implements ShouldQueue
|
||||
{
|
||||
use Queueable, SerializesModels;
|
||||
|
||||
/** The ledger row this mail confirms once actually delivered. */
|
||||
public ?int $notificationId = null;
|
||||
|
||||
public function __construct(
|
||||
public MaintenanceWindow $window,
|
||||
public Customer $customer,
|
||||
) {}
|
||||
|
||||
public function headers(): Headers
|
||||
{
|
||||
return new Headers(text: $this->notificationId ? ['X-CP-Notification' => (string) $this->notificationId] : []);
|
||||
}
|
||||
|
||||
public function envelope(): Envelope
|
||||
{
|
||||
return new Envelope(subject: __('maintenance.mail_subject', ['title' => $this->window->title]));
|
||||
|
|
|
|||
|
|
@ -9,17 +9,26 @@ use Illuminate\Contracts\Queue\ShouldQueue;
|
|||
use Illuminate\Mail\Mailable;
|
||||
use Illuminate\Mail\Mailables\Content;
|
||||
use Illuminate\Mail\Mailables\Envelope;
|
||||
use Illuminate\Mail\Mailables\Headers;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class MaintenanceCancelledMail extends Mailable implements ShouldQueue
|
||||
{
|
||||
use Queueable, SerializesModels;
|
||||
|
||||
/** The ledger row this mail confirms once actually delivered. */
|
||||
public ?int $notificationId = null;
|
||||
|
||||
public function __construct(
|
||||
public MaintenanceWindow $window,
|
||||
public Customer $customer,
|
||||
) {}
|
||||
|
||||
public function headers(): Headers
|
||||
{
|
||||
return new Headers(text: $this->notificationId ? ['X-CP-Notification' => (string) $this->notificationId] : []);
|
||||
}
|
||||
|
||||
public function envelope(): Envelope
|
||||
{
|
||||
return new Envelope(subject: __('maintenance.mail_cancel_subject', ['title' => $this->window->title]));
|
||||
|
|
|
|||
|
|
@ -15,6 +15,9 @@ use App\Services\Traefik\SshTraefikWriter;
|
|||
use App\Services\Traefik\TraefikWriter;
|
||||
use App\Services\Wireguard\LocalWireguardHub;
|
||||
use App\Services\Wireguard\WireguardHub;
|
||||
use App\Models\MaintenanceNotification;
|
||||
use Illuminate\Mail\Events\MessageSent;
|
||||
use Illuminate\Support\Facades\Event;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
|
||||
class AppServiceProvider extends ServiceProvider
|
||||
|
|
@ -42,6 +45,15 @@ class AppServiceProvider extends ServiceProvider
|
|||
*/
|
||||
public function boot(): void
|
||||
{
|
||||
//
|
||||
// Stamp a maintenance-notification ledger row as delivered only once the
|
||||
// mail is actually sent (the X-CP-Notification header carries the id).
|
||||
// Until then sent_at stays null → the row is a retryable marker.
|
||||
Event::listen(MessageSent::class, function (MessageSent $event) {
|
||||
$header = $event->message->getHeaders()->get('X-CP-Notification');
|
||||
if ($header === null) {
|
||||
return;
|
||||
}
|
||||
MaintenanceNotification::query()->whereKey((int) $header->getBodyAsString())->update(['sent_at' => now()]);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,13 +22,19 @@ class HostFactory extends Factory
|
|||
|
||||
public function active(): static
|
||||
{
|
||||
return $this->state(fn () => [
|
||||
'status' => 'active',
|
||||
'wg_ip' => '10.66.0.'.$this->faker->numberBetween(2, 250),
|
||||
'total_gb' => 1000,
|
||||
'total_ram_mb' => 65536,
|
||||
'cpu_cores' => 16,
|
||||
'cpu_weight' => 16,
|
||||
]);
|
||||
return $this->state(function () {
|
||||
// Unique WG IP over a large space — a plain random last octet collides
|
||||
// (birthday paradox) when a test creates several active hosts.
|
||||
$n = $this->faker->unique()->numberBetween(1, 16_000_000);
|
||||
|
||||
return [
|
||||
'status' => 'active',
|
||||
'wg_ip' => '10.'.(($n >> 16) & 255).'.'.(($n >> 8) & 255).'.'.($n & 255),
|
||||
'total_gb' => 1000,
|
||||
'total_ram_mb' => 65536,
|
||||
'cpu_cores' => 16,
|
||||
'cpu_weight' => 16,
|
||||
];
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue