app->singleton(PipelineRegistry::class, fn () => new PipelineRegistry( config('provisioning.pipelines', []), )); // Real I/O implementations; tests swap in fakes via app()->instance(). $this->app->bind(RemoteShell::class, PhpseclibRemoteShell::class); $this->app->bind(WireguardHub::class, LocalWireguardHub::class); $this->app->bind(ProxmoxClient::class, HttpProxmoxClient::class); $this->app->bind(HetznerDnsClient::class, HttpHetznerDnsClient::class); $this->app->bind(TraefikWriter::class, SshTraefikWriter::class); $this->app->bind(MonitoringClient::class, HttpMonitoringClient::class); } /** * Bootstrap any application services. */ public function boot(): void { // Send-time guard for maintenance mail (X-CP-Notification carries the // ledger id). Uses a distinct in-flight claim (claimed_at) so exactly one // copy ships without ever pre-marking delivery: // • already delivered (sent_at set) or window cancelled → suppress; // • otherwise atomically claim claimed_at (null or older than the TTL) — // the winner sends; a concurrent duplicate loses the claim and aborts. // sent_at is set only on real delivery (MessageSent), so a transport // failure leaves the claim to expire and the mail stays retryable. Event::listen(MessageSending::class, function (MessageSending $event) { $header = $event->message->getHeaders()->get('X-CP-Notification'); if ($header === null) { return null; } $id = (int) $header->getBodyAsString(); $notification = MaintenanceNotification::query()->with('window')->find($id); if ($notification === null) { return null; } if ($notification->sent_at !== null) { return false; // already delivered — suppress a duplicate copy } if ($notification->event === 'announcement' && $notification->window?->state === 'cancelled') { return false; // window cancelled meanwhile — do not deliver } $claimed = MaintenanceNotification::query() ->whereKey($id) ->whereNull('sent_at') ->where(fn ($q) => $q->whereNull('claimed_at')->orWhere('claimed_at', '<=', now()->subMinutes(15))) ->update(['claimed_at' => now()]); return $claimed === 0 ? false : null; // lost the claim → a sibling is delivering }); // 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. If an // announcement delivered for an already-cancelled window (the cancel race), // queue a catch-up cancellation so that customer isn't left mis-informed. Event::listen(MessageSent::class, function (MessageSent $event) { $header = $event->message->getHeaders()->get('X-CP-Notification'); if ($header === null) { return; } $notification = MaintenanceNotification::query()->with(['window', 'customer'])->find((int) $header->getBodyAsString()); if ($notification === null) { return; } $notification->update(['sent_at' => now()]); if ($notification->event === 'announcement' && $notification->window?->state === 'cancelled' && $notification->customer !== null) { app(MaintenanceNotifier::class)->deliver( $notification->window, $notification->customer, 'cancelled', new MaintenanceCancelledMail($notification->window, $notification->customer), ); } }); } }