From e6033538be9f78f39ab4432b6e9e015a5874279e Mon Sep 17 00:00:00 2001 From: HarlequinSin Date: Sun, 28 Dec 2025 13:03:13 +1100 Subject: [PATCH 01/51] add srv_target column to cloudflare_domains migration --- ...4_add_srv_target_to_cloudflare_domains.php | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 subdomains/database/migrations/004_add_srv_target_to_cloudflare_domains.php diff --git a/subdomains/database/migrations/004_add_srv_target_to_cloudflare_domains.php b/subdomains/database/migrations/004_add_srv_target_to_cloudflare_domains.php new file mode 100644 index 0000000..3cbe2a1 --- /dev/null +++ b/subdomains/database/migrations/004_add_srv_target_to_cloudflare_domains.php @@ -0,0 +1,22 @@ +string('srv_target')->nullable()->after('cloudflare_id'); + }); + } + + public function down(): void + { + Schema::table('cloudflare_domains', function (Blueprint $table) { + $table->dropColumn('srv_target'); + }); + } +}; From 5948e9c4b5994b29169fa5ab633fcae4de9ca1f8 Mon Sep 17 00:00:00 2001 From: HarlequinSin Date: Sun, 28 Dec 2025 13:03:34 +1100 Subject: [PATCH 02/51] add SRV record and target strings --- subdomains/lang/en/strings.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/subdomains/lang/en/strings.php b/subdomains/lang/en/strings.php index e1cd0ea..511bd80 100644 --- a/subdomains/lang/en/strings.php +++ b/subdomains/lang/en/strings.php @@ -14,6 +14,12 @@ 'name' => 'Name', + 'srv_record' => 'SRV Record', + 'srv_record_help' => 'Enable this option to create a SRV record instead of an A or AAAA record.', + + 'srv_target' => 'SRV Target', + 'srv_target_help' => 'The hostname that SRV records point to (for example: play.example.com).', + 'api_token' => 'Cloudflare API Token', 'api_token_help' => 'The token needs to have read permissions for Zone.Zone and write for Zone.Dns. For better security you can also set the "Zone Resources" to exclude certain domains and add the panel ip to the "Client IP Adress Filtering".', ]; From cc7665723a954b3c3eb82760ce89317f035174db Mon Sep 17 00:00:00 2001 From: HarlequinSin Date: Sun, 28 Dec 2025 13:04:14 +1100 Subject: [PATCH 03/51] add srv_target field to CloudflareDomain model and resource --- .../CloudflareDomains/CloudflareDomainResource.php | 7 +++++++ subdomains/src/Models/CloudflareDomain.php | 2 ++ 2 files changed, 9 insertions(+) diff --git a/subdomains/src/Filament/Admin/Resources/CloudflareDomains/CloudflareDomainResource.php b/subdomains/src/Filament/Admin/Resources/CloudflareDomains/CloudflareDomainResource.php index 571166f..7108768 100644 --- a/subdomains/src/Filament/Admin/Resources/CloudflareDomains/CloudflareDomainResource.php +++ b/subdomains/src/Filament/Admin/Resources/CloudflareDomains/CloudflareDomainResource.php @@ -53,6 +53,8 @@ public static function table(Table $table): Table ->columns([ TextColumn::make('name') ->label(trans('subdomains::strings.name')), + TextColumn::make('srv_target') + ->label(trans('subdomains::strings.srv_target')), TextColumn::make('subdomains_count') ->label(trans_choice('subdomains::strings.subdomain', 2)) ->counts('subdomains'), @@ -77,6 +79,11 @@ public static function form(Schema $schema): Schema ->label(trans('subdomains::strings.name')) ->required() ->unique(), + TextInput::make('srv_target') + ->label(trans('subdomains::strings.srv_target')) + ->helperText(trans('subdomains::strings.srv_target_help')) + ->placeholder('play.example.com') + ->rules(['nullable', 'string', 'regex:/^(?=.{1,253}$)(?!-)[A-Za-z0-9-]{1,63}(? Date: Sun, 28 Dec 2025 13:04:33 +1100 Subject: [PATCH 04/51] add srv_record toggle to SubdomainRelationManager and SubdomainResource --- .../SubdomainRelationManager.php | 7 +++++-- .../Resources/Subdomains/SubdomainResource.php | 16 +++++++++------- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/subdomains/src/Filament/Admin/Resources/Servers/RelationManagers/SubdomainRelationManager.php b/subdomains/src/Filament/Admin/Resources/Servers/RelationManagers/SubdomainRelationManager.php index 45de854..537fa05 100644 --- a/subdomains/src/Filament/Admin/Resources/Servers/RelationManagers/SubdomainRelationManager.php +++ b/subdomains/src/Filament/Admin/Resources/Servers/RelationManagers/SubdomainRelationManager.php @@ -12,6 +12,7 @@ use Filament\Forms\Components\Hidden; use Filament\Forms\Components\Select; use Filament\Forms\Components\TextInput; +use Filament\Forms\Components\Toggle; use Filament\Notifications\Notification; use Filament\Resources\RelationManagers\RelationManager; use Filament\Schemas\Schema; @@ -83,8 +84,10 @@ public function form(Schema $schema): Schema ->relationship('domain', 'name') ->preload() ->searchable(), - Hidden::make('record_type') - ->default(fn () => is_ipv6($this->getOwnerRecord()->allocation->ip) ? 'AAAA' : 'A'), + Toggle::make('srv_record') + ->label(trans('subdomains::strings.srv_record')) + ->helperText(trans('subdomains::strings.srv_record_help')) + ->default(false), ]); } } diff --git a/subdomains/src/Filament/Server/Resources/Subdomains/SubdomainResource.php b/subdomains/src/Filament/Server/Resources/Subdomains/SubdomainResource.php index ec372b3..992fedc 100644 --- a/subdomains/src/Filament/Server/Resources/Subdomains/SubdomainResource.php +++ b/subdomains/src/Filament/Server/Resources/Subdomains/SubdomainResource.php @@ -15,10 +15,12 @@ use Filament\Forms\Components\Hidden; use Filament\Forms\Components\Select; use Filament\Forms\Components\TextInput; +use Filament\Forms\Components\Toggle; use Filament\Resources\Resource; use Filament\Schemas\Schema; use Filament\Support\Enums\IconSize; use Filament\Tables\Columns\TextColumn; +use Filament\Tables\Columns\ToggleColumn; use Filament\Tables\Table; class SubdomainResource extends Resource @@ -78,6 +80,9 @@ public static function table(Table $table): Table TextColumn::make('label') ->label(trans('subdomains::strings.name')) ->state(fn (Subdomain $subdomain) => $subdomain->getLabel()), + ToggleColumn::make('srv_record') + ->label(trans('subdomains::strings.srv_record')) + ->tooltip(trans('subdomains::strings.srv_record_help')), ]) ->recordActions([ EditAction::make(), @@ -111,13 +116,10 @@ public static function form(Schema $schema): Schema ->relationship('domain', 'name') ->preload() ->searchable(), - Hidden::make('record_type') - ->default(function () { - /** @var Server $server */ - $server = Filament::getTenant(); - - return is_ipv6($server->allocation->ip) ? 'AAAA' : 'A'; - }), + Toggle::make('srv_record') + ->label(trans('subdomains::strings.srv_record')) + ->helperText(trans('subdomains::strings.srv_record_help')) + ->default(false), ]); } From 2e7246634d4fc1f449a3b47d34c132fb0862ba39 Mon Sep 17 00:00:00 2001 From: HarlequinSin Date: Sun, 28 Dec 2025 13:06:09 +1100 Subject: [PATCH 05/51] Adding logic for srv records --- subdomains/src/Models/Subdomain.php | 158 ++++++++++++++++++++++++++-- 1 file changed, 151 insertions(+), 7 deletions(-) diff --git a/subdomains/src/Models/Subdomain.php b/subdomains/src/Models/Subdomain.php index 14cd323..a85f92f 100644 --- a/subdomains/src/Models/Subdomain.php +++ b/subdomains/src/Models/Subdomain.php @@ -27,12 +27,45 @@ class Subdomain extends Model implements HasLabel 'cloudflare_id', 'domain_id', 'server_id', + 'srv_record', + ]; + + protected $casts = [ + 'srv_record' => 'boolean', ]; protected static function boot(): void { parent::boot(); + static::saving(function (self $model) { + // If srv_record provided in the payload, ensure record_type follows it and then remove it + if (array_key_exists('srv_record', $model->attributes)) { + $srv = (bool) $model->attributes['srv_record']; + + if ($srv) { + $model->attributes['record_type'] = 'SRV'; + } else { + if ($model->server && $model->server->allocation && is_ipv6($model->server->allocation->ip)) { + $model->attributes['record_type'] = 'AAAA'; + } else { + $model->attributes['record_type'] = 'A'; + } + } + + unset($model->attributes['srv_record']); + } + + // If no record_type is present, set a sensible default based on server allocation + if (!isset($model->attributes['record_type'])) { + if ($model->server && $model->server->allocation && is_ipv6($model->server->allocation->ip)) { + $model->attributes['record_type'] = 'AAAA'; + } else { + $model->attributes['record_type'] = 'A'; + } + } + }); + static::created(function (self $model) { $model->createOnCloudflare(); }); @@ -61,21 +94,95 @@ public function getLabel(): string|Htmlable|null return $this->name . '.' . $this->domain->name; } + public function getSrvRecordAttribute(): bool + { + return $this->record_type === 'SRV'; + } + + public function setSrvRecordAttribute($value): void + { + if ($value) { + $this->attributes['record_type'] = 'SRV'; + } else { + if ($this->server && $this->server->allocation && is_ipv6($this->server->allocation->ip)) { + $this->attributes['record_type'] = 'AAAA'; + } else { + $this->attributes['record_type'] = 'A'; + } + } + } + + protected function buildSrvPayload(): ?array + { + $target = $this->domain->srv_target ?? null; + $port = $this->server && $this->server->allocation ? ($this->server->allocation->port ?? null) : null; + + if (empty($target) || empty($port)) { + return null; + } + + $priority = (int) ($this->srv_priority ?? 0); + $weight = (int) ($this->srv_weight ?? 0); + $port = (int) $port; + + // Need to build the name to include the services and protocol parts for SRV records, this may vary based on game(egg tag)/server type + // Temporrary placeholder for service and protocol = '_minecraft._tcp.' + + return [ + 'name' => sprintf('_minecraft._tcp.%s', $this->name), + 'ttl' => 1, + 'type' => 'SRV', + 'comment' => 'Created by Pelican Subdomains plugin', + 'content' => sprintf('%d %d %d %s', $priority, $weight, $port, $target), + 'proxied' => false, + 'data' => [ + 'priority' => $priority, + 'weight' => $weight, + 'port' => $port, + 'target' => $target, + ], + ]; + } + protected function createOnCloudflare(): void { + if ($this->record_type === 'SRV') { + $payload = $this->buildSrvPayload(); + + if ($payload === null) { + return; + } + + if (!$this->cloudflare_id) { + $response = Http::cloudflare()->post("zones/{$this->domain->cloudflare_id}/dns_records", $payload)->json(); + + if (!empty($response['success'])) { + $dnsRecord = $response['result']; + + $this->updateQuietly([ + 'cloudflare_id' => $dnsRecord['id'], + ]); + } + } + + return; + } + if (!$this->server->allocation || $this->server->allocation->ip === '0.0.0.0' || $this->server->allocation->ip === '::') { return; } if (!$this->cloudflare_id) { - $response = Http::cloudflare()->post("zones/{$this->domain->cloudflare_id}/dns_records", [ + $body = [ 'name' => $this->name, - 'ttl' => 120, + 'ttl' => 1, 'type' => $this->record_type, 'comment' => 'Created by Pelican Subdomains plugin', 'content' => $this->server->allocation->ip, 'proxied' => false, - ])->json(); + ]; + + $response = Http::cloudflare()->post("zones/{$this->domain->cloudflare_id}/dns_records", $body)->json(); if ($response['success']) { $dnsRecord = $response['result']; @@ -93,16 +200,53 @@ protected function updateOnCloudflare(): void return; } - if ($this->cloudflare_id) { - Http::cloudflare()->patch("zones/{$this->domain->cloudflare_id}/dns_records/{$this->cloudflare_id}", [ + if ($this->record_type === 'SRV') { + $payload = $this->buildSrvPayload(); + + if ($payload === null) { + return; + } + + if ($this->cloudflare_id) { + Http::cloudflare()->put("zones/{$this->domain->cloudflare_id}/dns_records/{$this->cloudflare_id}", $payload); + } else { + $response = Http::cloudflare()->post("zones/{$this->domain->cloudflare_id}/dns_records", $payload)->json(); + + if (!empty($response['success'])) { + $dnsRecord = $response['result']; + + $this->updateQuietly([ + 'cloudflare_id' => $dnsRecord['id'], + ]); + } + } + + return; + } else { + $body = [ 'name' => $this->name, - 'ttl' => 120, + 'ttl' => 1, 'type' => $this->record_type, 'comment' => 'Created by Pelican Subdomains plugin', 'content' => $this->server->allocation->ip, 'proxied' => false, - ]); + ]; + + if ($this->cloudflare_id) { + Http::cloudflare()->put("zones/{$this->domain->cloudflare_id}/dns_records/{$this->cloudflare_id}", $body); + } else { + $response = Http::cloudflare()->post("zones/{$this->domain->cloudflare_id}/dns_records", $body)->json(); + + if (!empty($response['success'])) { + $dnsRecord = $response['result']; + + $this->updateQuietly([ + 'cloudflare_id' => $dnsRecord['id'], + ]); + } + } } + } protected function deleteOnCloudflare(): void From f20e58eb3ac8162632c097637fc8a75229a16734 Mon Sep 17 00:00:00 2001 From: HarlequinSin Date: Sun, 28 Dec 2025 13:53:09 +1100 Subject: [PATCH 06/51] add error messages for missing SRV target and enhance tooltip logic in SubdomainResource --- subdomains/lang/en/strings.php | 11 +++++++++++ .../Server/Resources/Subdomains/SubdomainResource.php | 4 +++- subdomains/src/Models/Subdomain.php | 2 ++ 3 files changed, 16 insertions(+), 1 deletion(-) diff --git a/subdomains/lang/en/strings.php b/subdomains/lang/en/strings.php index 511bd80..b81a42a 100644 --- a/subdomains/lang/en/strings.php +++ b/subdomains/lang/en/strings.php @@ -20,6 +20,17 @@ 'srv_target' => 'SRV Target', 'srv_target_help' => 'The hostname that SRV records point to (for example: play.example.com).', + 'errors' => [ + 'srv_target_missing' => 'Cannot enable SRV record because the selected domain does not have an SRV target set.', + ], + 'api_token' => 'Cloudflare API Token', 'api_token_help' => 'The token needs to have read permissions for Zone.Zone and write for Zone.Dns. For better security you can also set the "Zone Resources" to exclude certain domains and add the panel ip to the "Client IP Adress Filtering".', + + 'notifications' => [ + 'dns_created' => 'DNS record created on Cloudflare', + 'dns_updated' => 'DNS record updated on Cloudflare', + 'dns_deleted' => 'DNS record deleted from Cloudflare', + 'dns_action_failed' => 'Cloudflare DNS action failed', + ], ]; diff --git a/subdomains/src/Filament/Server/Resources/Subdomains/SubdomainResource.php b/subdomains/src/Filament/Server/Resources/Subdomains/SubdomainResource.php index 992fedc..b7f4313 100644 --- a/subdomains/src/Filament/Server/Resources/Subdomains/SubdomainResource.php +++ b/subdomains/src/Filament/Server/Resources/Subdomains/SubdomainResource.php @@ -82,7 +82,8 @@ public static function table(Table $table): Table ->state(fn (Subdomain $subdomain) => $subdomain->getLabel()), ToggleColumn::make('srv_record') ->label(trans('subdomains::strings.srv_record')) - ->tooltip(trans('subdomains::strings.srv_record_help')), + ->tooltip(fn (Subdomain $record) => $record->domain && $record->domain->srv_target ? trans('subdomains::strings.srv_record_help') : trans('subdomains::strings.errors.srv_target_missing')) + ->disabled(fn (Subdomain $record) => !$record->domain || empty($record->domain->srv_target)), ]) ->recordActions([ EditAction::make(), @@ -119,6 +120,7 @@ public static function form(Schema $schema): Schema Toggle::make('srv_record') ->label(trans('subdomains::strings.srv_record')) ->helperText(trans('subdomains::strings.srv_record_help')) + ->disabled(fn (callable $get) => !$get('domain_id') || empty(CloudflareDomain::find($get('domain_id'))->srv_target ?? null)) ->default(false), ]); } diff --git a/subdomains/src/Models/Subdomain.php b/subdomains/src/Models/Subdomain.php index a85f92f..b0ffe38 100644 --- a/subdomains/src/Models/Subdomain.php +++ b/subdomains/src/Models/Subdomain.php @@ -118,6 +118,8 @@ protected function buildSrvPayload(): ?array $port = $this->server && $this->server->allocation ? ($this->server->allocation->port ?? null) : null; if (empty($target) || empty($port)) { + // Output to log for debugging + Log::error('SRV record target or port is missing for Subdomain ID ' . $this->id . '. Target: ' . ($target ?? 'null') . ', Port: ' . ($port ?? 'null')); return null; } From 58de843308a8051078b6045aacf77fd610b77488 Mon Sep 17 00:00:00 2001 From: HarlequinSin Date: Sun, 28 Dec 2025 20:08:49 +1100 Subject: [PATCH 07/51] add zone request status messages and improve logging for missing SRV record target or port --- subdomains/lang/en/strings.php | 2 ++ subdomains/src/Models/Subdomain.php | 1 - 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/subdomains/lang/en/strings.php b/subdomains/lang/en/strings.php index b81a42a..3bc8625 100644 --- a/subdomains/lang/en/strings.php +++ b/subdomains/lang/en/strings.php @@ -32,5 +32,7 @@ 'dns_updated' => 'DNS record updated on Cloudflare', 'dns_deleted' => 'DNS record deleted from Cloudflare', 'dns_action_failed' => 'Cloudflare DNS action failed', + 'zone_request_failed' => 'Cloudflare zone request failed', + 'zone_request_succeeded' => 'Cloudflare zone request succeeded', ], ]; diff --git a/subdomains/src/Models/Subdomain.php b/subdomains/src/Models/Subdomain.php index b0ffe38..05a5ff9 100644 --- a/subdomains/src/Models/Subdomain.php +++ b/subdomains/src/Models/Subdomain.php @@ -118,7 +118,6 @@ protected function buildSrvPayload(): ?array $port = $this->server && $this->server->allocation ? ($this->server->allocation->port ?? null) : null; if (empty($target) || empty($port)) { - // Output to log for debugging Log::error('SRV record target or port is missing for Subdomain ID ' . $this->id . '. Target: ' . ($target ?? 'null') . ', Port: ' . ($port ?? 'null')); return null; } From 2ff7a9956ac1d7e2bfe9d9705eaf088681bb7dc9 Mon Sep 17 00:00:00 2001 From: HarlequinSin Date: Sun, 28 Dec 2025 22:39:59 +1100 Subject: [PATCH 08/51] add CloudflareService class for managing DNS records with upsert and delete functionality --- subdomains/src/Services/CloudflareService.php | 187 ++++++++++++++++++ 1 file changed, 187 insertions(+) create mode 100644 subdomains/src/Services/CloudflareService.php diff --git a/subdomains/src/Services/CloudflareService.php b/subdomains/src/Services/CloudflareService.php new file mode 100644 index 0000000..6b73b24 --- /dev/null +++ b/subdomains/src/Services/CloudflareService.php @@ -0,0 +1,187 @@ +get("zones/{$zoneId}/dns_records", [ + 'name' => $name, + 'type' => $type, + 'per_page' => 1, + ]); + } catch (\Throwable $e) { + Log::error('Cloudflare findDnsRecordId request failed: ' . $e->getMessage(), ['zone' => $zoneId, 'name' => $name, 'type' => $type]); + return null; + } + + $status = $response->status(); + $body = $response->json() ?? []; + + if ($response->successful() && !empty($body['result']) && count($body['result']) > 0) { + return $body['result'][0]['id'] ?? null; + } + + if (!empty($body['errors'])) { + Log::warning('Cloudflare findDnsRecordId returned errors', ['zone' => $zoneId, 'name' => $name, 'type' => $type, 'status' => $status, 'errors' => $body['errors']]); + } + + return null; + } + + public function upsertDnsRecord( + string $zoneId, + string $name, + string $recordType, + string $target = null, + ?int $port = null, + ): array + { + if (empty($zoneId) || empty($name) || empty($recordType)) { + Log::error('Cloudflare upsertDnsRecord missing required parameters', ['zone' => $zoneId, 'name' => $name, 'type' => $recordType]); + return ['success' => false, 'id' => null, 'errors' => ['missing_parameters' => true], 'status' => 0, 'body' => null]; + } + + // Hardcoded/derived defaults + $priority = 0; + $weight = 0; + $ttl = 1; + $comment = 'Created by Pelican Subdomains plugin'; + $proxied = false; + + // Build payload based on type + if ($recordType === 'SRV') { + if (empty($port) || empty($target)) { + Log::error('Cloudflare upsert missing SRV target or port', ['zone' => $zoneId, 'name' => $name, 'type' => $recordType]); + return ['success' => false, 'id' => null, 'errors' => ['missing_srv_target_or_port' => true], 'status' => 0, 'body' => null]; + } + + $payload = [ + 'name' => sprintf('_minecraft._tcp.%s', $name), + 'ttl' => $ttl, + 'type' => 'SRV', + 'comment' => $comment, + 'content' => sprintf('%d %d %d %s', $priority, $weight, $port, $target), + 'proxied' => $proxied, + 'data' => [ + 'priority' => $priority, + 'weight' => $weight, + 'port' => (int) $port, + 'target' => $target, + ], + ]; + } else { + $ip = $content ?? $target; + + if (empty($ip)) { + Log::error('Cloudflare upsert missing IP/content for record type ' . $recordType, ['zone' => $zoneId, 'name' => $name, 'type' => $recordType]); + return ['success' => false, 'id' => null, 'errors' => ['missing_ip' => true], 'status' => 0, 'body' => null]; + } + + $payload = [ + 'name' => $name, + 'ttl' => $ttl, + 'type' => $recordType, + 'comment' => $comment, + 'content' => $target, + 'proxied' => $proxied, + ]; + } + + $existingId = $this->findDnsRecordId($zoneId, $payload['name'], $payload['type']); + + try { + if ($existingId) { + $response = Http::cloudflare()->put("zones/{$zoneId}/dns_records/{$existingId}", $payload); + $parsed = $this->parseCloudflareHttpResponse($response); + + if ($parsed['success']) { + return $parsed; + } + + Log::error('Cloudflare update failed', ['zone' => $zoneId, 'id' => $existingId, 'response' => $parsed]); + return $parsed; + } + + $response = Http::cloudflare()->post("zones/{$zoneId}/dns_records", $payload); + $parsed = $this->parseCloudflareHttpResponse($response); + + if ($parsed['success'] && !empty($parsed['id'])) { + return $parsed; + } + + Log::error('Cloudflare create failed', ['zone' => $zoneId, 'payload' => $payload, 'response' => $parsed]); + return $parsed; + } catch (\Throwable $e) { + Log::error('Cloudflare upsert exception: ' . $e->getMessage(), ['zone' => $zoneId, 'payload' => $payload]); + return ['success' => false, 'id' => null, 'errors' => ['exception' => $e->getMessage()], 'status' => 0, 'body' => null]; + } + } + + + protected function parseCloudflareResponse(array $response): array + { + return [ + 'success' => !empty($response['success']), + 'id' => $response['result']['id'] ?? null, + 'result' => $response['result'] ?? null, + 'errors' => $response['errors'] ?? [], + ]; + } + + /** + * Parse a Response object into a normalized structure with HTTP status and body. + */ + protected function parseCloudflareHttpResponse(Response $response): array + { + $status = $response->status(); + $body = $response->json() ?? []; + + $success = $response->successful() && (!empty($body['success']) || !empty($body['result'])); + + return [ + 'success' => $success, + 'id' => $body['result']['id'] ?? null, + 'errors' => $body['errors'] ?? [], + 'status' => $status, + 'body' => $body, + ]; + } + + + /** + * Delete a DNS record by id. Returns a detailed response array. + */ + public function deleteDnsRecord(string $zoneId, string $recordId): array + { + if (empty($zoneId) || empty($recordId)) { + return ['success' => false, 'errors' => ['missing_parameters' => true], 'status' => 0, 'body' => null]; + } + + try { + $response = Http::cloudflare()->delete("zones/{$zoneId}/dns_records/{$recordId}"); + + $parsed = $this->parseCloudflareHttpResponse($response); + + if ($parsed['success']) { + return $parsed; + } + + Log::error('Cloudflare delete failed', ['zone' => $zoneId, 'id' => $recordId, 'response' => $parsed]); + return $parsed; + } catch (\Throwable $e) { + Log::error('Cloudflare delete exception: ' . $e->getMessage(), ['zone' => $zoneId, 'id' => $recordId]); + return ['success' => false, 'errors' => ['exception' => $e->getMessage()], 'status' => 0, 'body' => null]; + } + } +} From 15721872d7477e2bb85de50d94ca30417a43887c Mon Sep 17 00:00:00 2001 From: HarlequinSin Date: Sun, 28 Dec 2025 22:49:38 +1100 Subject: [PATCH 09/51] cleanup --- subdomains/src/Services/CloudflareService.php | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/subdomains/src/Services/CloudflareService.php b/subdomains/src/Services/CloudflareService.php index 6b73b24..15b0307 100644 --- a/subdomains/src/Services/CloudflareService.php +++ b/subdomains/src/Services/CloudflareService.php @@ -43,7 +43,7 @@ public function upsertDnsRecord( string $zoneId, string $name, string $recordType, - string $target = null, + string $target, ?int $port = null, ): array { @@ -139,9 +139,6 @@ protected function parseCloudflareResponse(array $response): array ]; } - /** - * Parse a Response object into a normalized structure with HTTP status and body. - */ protected function parseCloudflareHttpResponse(Response $response): array { $status = $response->status(); @@ -159,9 +156,6 @@ protected function parseCloudflareHttpResponse(Response $response): array } - /** - * Delete a DNS record by id. Returns a detailed response array. - */ public function deleteDnsRecord(string $zoneId, string $recordId): array { if (empty($zoneId) || empty($recordId)) { From eb1a0ebbb28b5faf6b34793e99350a08eedb7be7 Mon Sep 17 00:00:00 2001 From: HarlequinSin Date: Sun, 28 Dec 2025 22:50:18 +1100 Subject: [PATCH 10/51] Remade script to utilize upsert with better error handling --- subdomains/src/Models/Subdomain.php | 196 ++++++++++++---------------- 1 file changed, 80 insertions(+), 116 deletions(-) diff --git a/subdomains/src/Models/Subdomain.php b/subdomains/src/Models/Subdomain.php index 05a5ff9..514dc0c 100644 --- a/subdomains/src/Models/Subdomain.php +++ b/subdomains/src/Models/Subdomain.php @@ -8,6 +8,9 @@ use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Support\Facades\Http; +use Illuminate\Support\Facades\Log; +use Filament\Notifications\Notification; +use Boy132\Subdomains\Services\CloudflareService; /** * @property int $id @@ -66,12 +69,8 @@ protected static function boot(): void } }); - static::created(function (self $model) { - $model->createOnCloudflare(); - }); - - static::updated(function (self $model) { - $model->updateOnCloudflare(); + static::saved(function (self $model) { + $model->upsertOnCloudflare(); }); static::deleted(function (self $model) { @@ -99,6 +98,8 @@ public function getSrvRecordAttribute(): bool return $this->record_type === 'SRV'; } + + public function setSrvRecordAttribute($value): void { if ($value) { @@ -112,148 +113,111 @@ public function setSrvRecordAttribute($value): void } } - protected function buildSrvPayload(): ?array + + + protected function upsertOnCloudflare(): void { - $target = $this->domain->srv_target ?? null; - $port = $this->server && $this->server->allocation ? ($this->server->allocation->port ?? null) : null; + $service = app(CloudflareService::class); - if (empty($target) || empty($port)) { - Log::error('SRV record target or port is missing for Subdomain ID ' . $this->id . '. Target: ' . ($target ?? 'null') . ', Port: ' . ($port ?? 'null')); - return null; - } + $zoneId = $this->domain->cloudflare_id; + if (empty($zoneId)) { + Log::warning('Cloudflare zone id missing for domain', ['domain_id' => $this->domain_id]); - $priority = (int) ($this->srv_priority ?? 0); - $weight = (int) ($this->srv_weight ?? 0); - $port = (int) $port; - - // Need to build the name to include the services and protocol parts for SRV records, this may vary based on game(egg tag)/server type - // Temporrary placeholder for service and protocol = '_minecraft._tcp.' - - return [ - 'name' => sprintf('_minecraft._tcp.%s', $this->name), - 'ttl' => 1, - 'type' => 'SRV', - 'comment' => 'Created by Pelican Subdomains plugin', - 'content' => sprintf('%d %d %d %s', $priority, $weight, $port, $target), - 'proxied' => false, - 'data' => [ - 'priority' => $priority, - 'weight' => $weight, - 'port' => $port, - 'target' => $target, - ], - ]; - } + Notification::make() + ->danger() + ->title('Cloudflare: Missing Zone ID') + ->body(sprintf('Cloudflare zone ID is not configured for %s. Cannot upsert DNS record for %s.%s.', $this->domain->name ?? 'unknown', $this->name, $this->domain->name ?? 'unknown')) + ->send(); - protected function createOnCloudflare(): void - { + return; + } + + // SRV: target comes from domain, port from server allocation if ($this->record_type === 'SRV') { - $payload = $this->buildSrvPayload(); + $port = $this->server && $this->server->allocation ? ($this->server->allocation->port ?? null) : null; + + if (empty($port)) { + Log::warning('Server missing allocation with port', ['server_id' => $this->server_id]); + + Notification::make() + ->danger() + ->title('Cloudflare: Missing SRV Port') + ->body(sprintf('SRV target or port is missing for %s.%s. Cannot upsert SRV record.', $this->name, $this->domain->name ?? 'unknown')) + ->send(); - if ($payload === null) { return; } - if (!$this->cloudflare_id) { - $response = Http::cloudflare()->post("zones/{$this->domain->cloudflare_id}/dns_records", $payload)->json(); - - if (!empty($response['success'])) { - $dnsRecord = $response['result']; + $result = $service->upsertDnsRecord($zoneId, $this->name, 'SRV', $this->domain->srv_target, $port); - $this->updateQuietly([ - 'cloudflare_id' => $dnsRecord['id'], - ]); + if ($result['success'] && !empty($result['id'])) { + if ($this->cloudflare_id !== $result['id']) { + $this->updateQuietly(['cloudflare_id' => $result['id']]); } + } else { + Log::error('Failed to upsert SRV record on Cloudflare for Subdomain ID ' . $this->id, ['result' => $result]); + + Notification::make() + ->danger() + ->title('Cloudflare: SRV upsert failed') + ->body('Failed to upsert SRV record for ' . $this->name . '.' . ($this->domain->name ?? 'unknown') . '. See logs for details. Errors: ' . json_encode($result['errors'] ?? $result['body'] ?? [])) + ->send(); } return; } + // A/AAAA if (!$this->server->allocation || $this->server->allocation->ip === '0.0.0.0' || $this->server->allocation->ip === '::') { return; } - if (!$this->cloudflare_id) { - $body = [ - 'name' => $this->name, - 'ttl' => 1, - 'type' => $this->record_type, - 'comment' => 'Created by Pelican Subdomains plugin', - 'content' => $this->server->allocation->ip, - 'proxied' => false, - ]; + $result = $service->upsertDnsRecord($zoneId, $this->name, $this->record_type, $ip, null, null, $this->domain->name); - $response = Http::cloudflare()->post("zones/{$this->domain->cloudflare_id}/dns_records", $body)->json(); + if ($result['success'] && !empty($result['id'])) { + if ($this->cloudflare_id !== $result['id']) { + $this->updateQuietly(['cloudflare_id' => $result['id']]); + } + } else { + Log::error('Failed to upsert record on Cloudflare for Subdomain ID ' . $this->id, ['result' => $result]); - if ($response['success']) { - $dnsRecord = $response['result']; + $domainName = $this->domain->name ?? 'unknown'; + $sub = sprintf('%s.%s', $this->name, $domainName); - $this->updateQuietly([ - 'cloudflare_id' => $dnsRecord['id'], - ]); - } + Notification::make() + ->danger() + ->title('Cloudflare: Upsert failed') + ->body('Failed to upsert record for ' . $sub . '. See logs for details. Errors: ' . json_encode($result['errors'] ?? $result['body'] ?? [])) + ->send(); } } - protected function updateOnCloudflare(): void - { - if (!$this->server->allocation || $this->server->allocation->ip === '0.0.0.0' || $this->server->allocation->ip === '::') { - return; - } - - if ($this->record_type === 'SRV') { - $payload = $this->buildSrvPayload(); - if ($payload === null) { - return; - } - if ($this->cloudflare_id) { - Http::cloudflare()->put("zones/{$this->domain->cloudflare_id}/dns_records/{$this->cloudflare_id}", $payload); - } else { - $response = Http::cloudflare()->post("zones/{$this->domain->cloudflare_id}/dns_records", $payload)->json(); - - if (!empty($response['success'])) { - $dnsRecord = $response['result']; + protected function deleteOnCloudflare(): void + { + if ($this->cloudflare_id && $this->domain && $this->domain->cloudflare_id) { + $service = app(CloudflareService::class); - $this->updateQuietly([ - 'cloudflare_id' => $dnsRecord['id'], - ]); - } - } + $result = $service->deleteDnsRecord($this->domain->cloudflare_id, $this->cloudflare_id); - return; - } else { - $body = [ - 'name' => $this->name, - 'ttl' => 1, - 'type' => $this->record_type, - 'comment' => 'Created by Pelican Subdomains plugin', - 'content' => $this->server->allocation->ip, - 'proxied' => false, - ]; - - if ($this->cloudflare_id) { - Http::cloudflare()->put("zones/{$this->domain->cloudflare_id}/dns_records/{$this->cloudflare_id}", $body); + if (!empty($result['success'])) { + Log::info('Deleted Cloudflare record for Subdomain ID ' . $this->id, ['result' => $result]); } else { - $response = Http::cloudflare()->post("zones/{$this->domain->cloudflare_id}/dns_records", $body)->json(); - - if (!empty($response['success'])) { - $dnsRecord = $response['result']; - - $this->updateQuietly([ - 'cloudflare_id' => $dnsRecord['id'], - ]); - } + Log::warning('Failed to delete Cloudflare record for Subdomain ID ' . $this->id, ['result' => $result]); + + Notification::make() + ->danger() + ->title('Cloudflare: Delete failed') + ->body('Failed to delete Cloudflare record for ' . $this->name . '.' . ($this->domain->name ?? 'unknown') . '. See logs for details. Errors: ' . json_encode($result['errors'] ?? $result['body'] ?? [])) + ->send(); + + Notification::make() + ->danger() + ->title('Cloudflare: Upsert failed') + ->body('Failed to upsert record for ' . $this->name . '.' . ($this->domain->name ?? 'unknown') . '. See logs for details. Errors: ' . json_encode($result['errors'] ?? $result['body'] ?? [])) + ->send(); } } - - } - - protected function deleteOnCloudflare(): void - { - if ($this->cloudflare_id) { - Http::cloudflare()->delete("zones/{$this->domain->cloudflare_id}/dns_records/{$this->cloudflare_id}"); - } } } From 6951bc70c44ce21109d11bf73143f7a6661eb5e1 Mon Sep 17 00:00:00 2001 From: HarlequinSin Date: Mon, 29 Dec 2025 08:11:36 +1100 Subject: [PATCH 11/51] Update to CloudflareDomain model to fetch Cloudflare Zone ID with notifications on success or failure --- .../Pages/ManageCloudflareDomains.php | 3 +- subdomains/src/Models/CloudflareDomain.php | 38 +++++++-------- subdomains/src/Services/CloudflareService.php | 46 ++++++++++++++++--- 3 files changed, 62 insertions(+), 25 deletions(-) diff --git a/subdomains/src/Filament/Admin/Resources/CloudflareDomains/Pages/ManageCloudflareDomains.php b/subdomains/src/Filament/Admin/Resources/CloudflareDomains/Pages/ManageCloudflareDomains.php index 238e6fe..7a0dbf4 100644 --- a/subdomains/src/Filament/Admin/Resources/CloudflareDomains/Pages/ManageCloudflareDomains.php +++ b/subdomains/src/Filament/Admin/Resources/CloudflareDomains/Pages/ManageCloudflareDomains.php @@ -14,7 +14,8 @@ protected function getHeaderActions(): array { return [ CreateAction::make() - ->createAnother(false), + ->createAnother(false) + ->successNotification(null), ]; } } diff --git a/subdomains/src/Models/CloudflareDomain.php b/subdomains/src/Models/CloudflareDomain.php index c510959..134e417 100644 --- a/subdomains/src/Models/CloudflareDomain.php +++ b/subdomains/src/Models/CloudflareDomain.php @@ -5,6 +5,8 @@ use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Support\Facades\Http; +use Boy132\Subdomains\Services\CloudflareService; +use Filament\Notifications\Notification; /** * @property int $id @@ -25,7 +27,24 @@ protected static function boot(): void parent::boot(); static::created(function (self $model) { - $model->fetchCloudflareId(); + $service = new CloudflareService(); + + $zoneId = $service->getZoneId($model->name); + if (!$zoneId) { + Notification::make() + ->title('Failed to fetch Cloudflare Zone ID for domain: ' . $model->name) + ->danger() + ->send(); + } + + Notification::make() + ->title('Successfully saved domain: ' . $model->name) + ->success() + ->send(); + + $model->update([ + 'cloudflare_id' => $zoneId, + ]); }); } @@ -33,21 +52,4 @@ public function subdomains(): HasMany { return $this->hasMany(Subdomain::class, 'domain_id'); } - - public function fetchCloudflareId(): void - { - $response = Http::cloudflare()->get('zones', [ - 'name' => $this->name, - ])->json(); - - if ($response['success']) { - $zones = $response['result']; - - if (count($zones) > 0) { - $this->update([ - 'cloudflare_id' => $zones[0]['id'], - ]); - } - } - } } diff --git a/subdomains/src/Services/CloudflareService.php b/subdomains/src/Services/CloudflareService.php index 15b0307..0bfa7d9 100644 --- a/subdomains/src/Services/CloudflareService.php +++ b/subdomains/src/Services/CloudflareService.php @@ -8,23 +8,57 @@ class CloudflareService { - public function findDnsRecordId(string $zoneId, string $name, string $type): ?string + public function getZoneId(string $domainName): ?string { - if (empty($zoneId)) { + if (empty($domainName)) { + Log::error('Cloudflare getZoneId called with empty domain name', ['domain' => $domainName]); return null; } try { - $response = Http::cloudflare()->get("zones/{$zoneId}/dns_records", [ + $response = Http::cloudflare()->get('zones', [ + 'name' => $domainName, + ]); + } catch (\Throwable $e) { + Log::error('Cloudflare getZoneId request failed: ' . $e->getMessage(), ['domain' => $domainName]); + return null; + } + + $status = $response->status(); + $body = $response->json() ?? []; + + if ($response->successful() && !empty($body['result']) && count($body['result']) > 0) { + return $body['result'][0]['id'] ?? null; + } + + if (!empty($body['errors'])) { + Log::warning('Cloudflare getZoneId returned errors', ['domain' => $domainName, 'status' => $status, 'errors' => $body['errors']]); + } + + return null; + } + + public function getDnsRecordId(string $zoneId, string $name, string $type): ?string + { + if (empty($zoneId) || empty($name)) { + return null; + } + + try { + $queryParams = [ 'name' => $name, 'type' => $type, 'per_page' => 1, - ]); + ]; + + $response = Http::cloudflare()->get("zones/{$zoneId}/dns_records"); } catch (\Throwable $e) { - Log::error('Cloudflare findDnsRecordId request failed: ' . $e->getMessage(), ['zone' => $zoneId, 'name' => $name, 'type' => $type]); + Log::error('Cloudflare getDnsRecordId request failed: ' . $e->getMessage(), ['zone' => $zoneId, 'name' => $name, 'type' => $type]); return null; } + log::warning('record response: '.$response); // !remove + log::warning($payload); // !remove $status = $response->status(); $body = $response->json() ?? []; @@ -33,7 +67,7 @@ public function findDnsRecordId(string $zoneId, string $name, string $type): ?st } if (!empty($body['errors'])) { - Log::warning('Cloudflare findDnsRecordId returned errors', ['zone' => $zoneId, 'name' => $name, 'type' => $type, 'status' => $status, 'errors' => $body['errors']]); + Log::warning('Cloudflare getDnsRecordId returned errors', ['zone' => $zoneId, 'name' => $name, 'type' => $type, 'status' => $status, 'errors' => $body['errors']]); } return null; From f18101a755d2068dc2f82ba4ae7268e153118a7f Mon Sep 17 00:00:00 2001 From: HarlequinSin Date: Mon, 29 Dec 2025 08:40:41 +1100 Subject: [PATCH 12/51] Changed upsert to require a recordId to be known (prevents changing records created outside plugin) --- subdomains/src/Models/Subdomain.php | 4 +- subdomains/src/Services/CloudflareService.php | 44 ++----------------- 2 files changed, 6 insertions(+), 42 deletions(-) diff --git a/subdomains/src/Models/Subdomain.php b/subdomains/src/Models/Subdomain.php index 514dc0c..01aaf4e 100644 --- a/subdomains/src/Models/Subdomain.php +++ b/subdomains/src/Models/Subdomain.php @@ -148,7 +148,7 @@ protected function upsertOnCloudflare(): void return; } - $result = $service->upsertDnsRecord($zoneId, $this->name, 'SRV', $this->domain->srv_target, $port); + $result = $service->upsertDnsRecord($zoneId, $this->name, 'SRV', $this->domain->srv_target, $this->cloudflare_id ?? null, $port); if ($result['success'] && !empty($result['id'])) { if ($this->cloudflare_id !== $result['id']) { @@ -172,7 +172,7 @@ protected function upsertOnCloudflare(): void return; } - $result = $service->upsertDnsRecord($zoneId, $this->name, $this->record_type, $ip, null, null, $this->domain->name); + $result = $service->upsertDnsRecord($zoneId, $this->name, $this->record_type, $this->server->allocation->ip, $this->cloudflare_id ?? null, null); if ($result['success'] && !empty($result['id'])) { if ($this->cloudflare_id !== $result['id']) { diff --git a/subdomains/src/Services/CloudflareService.php b/subdomains/src/Services/CloudflareService.php index 0bfa7d9..9fb0b99 100644 --- a/subdomains/src/Services/CloudflareService.php +++ b/subdomains/src/Services/CloudflareService.php @@ -38,46 +38,12 @@ public function getZoneId(string $domainName): ?string return null; } - public function getDnsRecordId(string $zoneId, string $name, string $type): ?string - { - if (empty($zoneId) || empty($name)) { - return null; - } - - try { - $queryParams = [ - 'name' => $name, - 'type' => $type, - 'per_page' => 1, - ]; - - $response = Http::cloudflare()->get("zones/{$zoneId}/dns_records"); - } catch (\Throwable $e) { - Log::error('Cloudflare getDnsRecordId request failed: ' . $e->getMessage(), ['zone' => $zoneId, 'name' => $name, 'type' => $type]); - return null; - } - - log::warning('record response: '.$response); // !remove - log::warning($payload); // !remove - $status = $response->status(); - $body = $response->json() ?? []; - - if ($response->successful() && !empty($body['result']) && count($body['result']) > 0) { - return $body['result'][0]['id'] ?? null; - } - - if (!empty($body['errors'])) { - Log::warning('Cloudflare getDnsRecordId returned errors', ['zone' => $zoneId, 'name' => $name, 'type' => $type, 'status' => $status, 'errors' => $body['errors']]); - } - - return null; - } - public function upsertDnsRecord( string $zoneId, string $name, string $recordType, string $target, + ?string $recordId = null, ?int $port = null, ): array { @@ -132,18 +98,16 @@ public function upsertDnsRecord( ]; } - $existingId = $this->findDnsRecordId($zoneId, $payload['name'], $payload['type']); - try { - if ($existingId) { - $response = Http::cloudflare()->put("zones/{$zoneId}/dns_records/{$existingId}", $payload); + if ($recordId) { + $response = Http::cloudflare()->put("zones/{$zoneId}/dns_records/{$recordId}", $payload); $parsed = $this->parseCloudflareHttpResponse($response); if ($parsed['success']) { return $parsed; } - Log::error('Cloudflare update failed', ['zone' => $zoneId, 'id' => $existingId, 'response' => $parsed]); + Log::error('Cloudflare update failed', ['zone' => $zoneId, 'recordId' => $recordId, 'response' => $parsed]); return $parsed; } From 58f73e170f629d206d582f0a33961dd20ba618ad Mon Sep 17 00:00:00 2001 From: HarlequinSin Date: Mon, 29 Dec 2025 08:41:19 +1100 Subject: [PATCH 13/51] Cleanup --- subdomains/src/Models/Subdomain.php | 31 ++----------------- subdomains/src/Services/CloudflareService.php | 18 ----------- 2 files changed, 3 insertions(+), 46 deletions(-) diff --git a/subdomains/src/Models/Subdomain.php b/subdomains/src/Models/Subdomain.php index 01aaf4e..9b1342a 100644 --- a/subdomains/src/Models/Subdomain.php +++ b/subdomains/src/Models/Subdomain.php @@ -42,38 +42,15 @@ protected static function boot(): void parent::boot(); static::saving(function (self $model) { - // If srv_record provided in the payload, ensure record_type follows it and then remove it if (array_key_exists('srv_record', $model->attributes)) { - $srv = (bool) $model->attributes['srv_record']; - - if ($srv) { - $model->attributes['record_type'] = 'SRV'; - } else { - if ($model->server && $model->server->allocation && is_ipv6($model->server->allocation->ip)) { - $model->attributes['record_type'] = 'AAAA'; - } else { - $model->attributes['record_type'] = 'A'; - } - } - + $model->setRecordType($model->attributes['srv_record']); unset($model->attributes['srv_record']); } - // If no record_type is present, set a sensible default based on server allocation - if (!isset($model->attributes['record_type'])) { - if ($model->server && $model->server->allocation && is_ipv6($model->server->allocation->ip)) { - $model->attributes['record_type'] = 'AAAA'; - } else { - $model->attributes['record_type'] = 'A'; - } - } - }); - - static::saved(function (self $model) { $model->upsertOnCloudflare(); }); - static::deleted(function (self $model) { + static::deleting(function (self $model) { $model->deleteOnCloudflare(); }); } @@ -98,9 +75,7 @@ public function getSrvRecordAttribute(): bool return $this->record_type === 'SRV'; } - - - public function setSrvRecordAttribute($value): void + public function setRecordType($value): void { if ($value) { $this->attributes['record_type'] = 'SRV'; diff --git a/subdomains/src/Services/CloudflareService.php b/subdomains/src/Services/CloudflareService.php index 9fb0b99..f683714 100644 --- a/subdomains/src/Services/CloudflareService.php +++ b/subdomains/src/Services/CloudflareService.php @@ -81,13 +81,6 @@ public function upsertDnsRecord( ], ]; } else { - $ip = $content ?? $target; - - if (empty($ip)) { - Log::error('Cloudflare upsert missing IP/content for record type ' . $recordType, ['zone' => $zoneId, 'name' => $name, 'type' => $recordType]); - return ['success' => false, 'id' => null, 'errors' => ['missing_ip' => true], 'status' => 0, 'body' => null]; - } - $payload = [ 'name' => $name, 'ttl' => $ttl, @@ -126,17 +119,6 @@ public function upsertDnsRecord( } } - - protected function parseCloudflareResponse(array $response): array - { - return [ - 'success' => !empty($response['success']), - 'id' => $response['result']['id'] ?? null, - 'result' => $response['result'] ?? null, - 'errors' => $response['errors'] ?? [], - ]; - } - protected function parseCloudflareHttpResponse(Response $response): array { $status = $response->status(); From 89f45b8338ce9a6e48de17d8c1074d2d1410b7dd Mon Sep 17 00:00:00 2001 From: HarlequinSin Date: Mon, 29 Dec 2025 08:41:32 +1100 Subject: [PATCH 14/51] Fixing notifications --- subdomains/src/Models/Subdomain.php | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/subdomains/src/Models/Subdomain.php b/subdomains/src/Models/Subdomain.php index 9b1342a..3980389 100644 --- a/subdomains/src/Models/Subdomain.php +++ b/subdomains/src/Models/Subdomain.php @@ -129,6 +129,12 @@ protected function upsertOnCloudflare(): void if ($this->cloudflare_id !== $result['id']) { $this->updateQuietly(['cloudflare_id' => $result['id']]); } + + Notification::make() + ->success() + ->title('Cloudflare: Record updated') + ->body('Successfully updated ' . $this->name . '.' . ($this->domain->name ?? 'unknown') . ' to '. $this->record_type) + ->send(); } else { Log::error('Failed to upsert SRV record on Cloudflare for Subdomain ID ' . $this->id, ['result' => $result]); @@ -144,6 +150,14 @@ protected function upsertOnCloudflare(): void // A/AAAA if (!$this->server->allocation || $this->server->allocation->ip === '0.0.0.0' || $this->server->allocation->ip === '::') { + Log::warning('Server allocation missing or invalid IP', ['server_id' => $this->server_id]); + + Notification::make() + ->danger() + ->title('Cloudflare: Missing IP') + ->body(sprintf('Server allocation IP is missing or invalid for %s.%s. Cannot upsert A/AAAA record.', $this->name, $this->domain->name ?? 'unknown')) + ->send(); + return; } @@ -153,6 +167,12 @@ protected function upsertOnCloudflare(): void if ($this->cloudflare_id !== $result['id']) { $this->updateQuietly(['cloudflare_id' => $result['id']]); } + + Notification::make() + ->success() + ->title('Cloudflare: Record updated') + ->body('Successfully updated ' . $this->name . '.' . ($this->domain->name ?? 'unknown') . ' to '. $this->record_type) + ->send(); } else { Log::error('Failed to upsert record on Cloudflare for Subdomain ID ' . $this->id, ['result' => $result]); From d369cc3039027f649507bdfcc2f42caac2507949 Mon Sep 17 00:00:00 2001 From: HarlequinSin Date: Mon, 29 Dec 2025 09:57:36 +1100 Subject: [PATCH 15/51] - Fixed issue with create record as SRV - Fixed potential potential NPE - Corrected some notifications --- subdomains/src/Models/Subdomain.php | 26 ++++++++++---------------- 1 file changed, 10 insertions(+), 16 deletions(-) diff --git a/subdomains/src/Models/Subdomain.php b/subdomains/src/Models/Subdomain.php index 3980389..a78125f 100644 --- a/subdomains/src/Models/Subdomain.php +++ b/subdomains/src/Models/Subdomain.php @@ -46,11 +46,13 @@ protected static function boot(): void $model->setRecordType($model->attributes['srv_record']); unset($model->attributes['srv_record']); } + }); + static::saved(function (self $model) { $model->upsertOnCloudflare(); }); - static::deleting(function (self $model) { + static::deleted(function (self $model) { $model->deleteOnCloudflare(); }); } @@ -88,8 +90,6 @@ public function setRecordType($value): void } } - - protected function upsertOnCloudflare(): void { $service = app(CloudflareService::class); @@ -112,7 +112,7 @@ protected function upsertOnCloudflare(): void $port = $this->server && $this->server->allocation ? ($this->server->allocation->port ?? null) : null; if (empty($port)) { - Log::warning('Server missing allocation with port', ['server_id' => $this->server_id]); + Log::warning('Server missing allocation with port', $this->toArray()); Notification::make() ->danger() @@ -149,7 +149,7 @@ protected function upsertOnCloudflare(): void } // A/AAAA - if (!$this->server->allocation || $this->server->allocation->ip === '0.0.0.0' || $this->server->allocation->ip === '::') { + if (!$this->server || !$this->server->allocation || $this->server->allocation->ip === '0.0.0.0' || $this->server->allocation->ip === '::') { Log::warning('Server allocation missing or invalid IP', ['server_id' => $this->server_id]); Notification::make() @@ -187,8 +187,6 @@ protected function upsertOnCloudflare(): void } } - - protected function deleteOnCloudflare(): void { if ($this->cloudflare_id && $this->domain && $this->domain->cloudflare_id) { @@ -197,22 +195,18 @@ protected function deleteOnCloudflare(): void $result = $service->deleteDnsRecord($this->domain->cloudflare_id, $this->cloudflare_id); if (!empty($result['success'])) { - Log::info('Deleted Cloudflare record for Subdomain ID ' . $this->id, ['result' => $result]); - } else { - Log::warning('Failed to delete Cloudflare record for Subdomain ID ' . $this->id, ['result' => $result]); - Notification::make() ->danger() - ->title('Cloudflare: Delete failed') - ->body('Failed to delete Cloudflare record for ' . $this->name . '.' . ($this->domain->name ?? 'unknown') . '. See logs for details. Errors: ' . json_encode($result['errors'] ?? $result['body'] ?? [])) + ->title('Cloudflare: Delete successed') + ->body('Successfully deleted Cloudflare record for ' . $this->name . '.' . ($this->domain->name ?? 'unknown') . '.') ->send(); + } Notification::make() ->danger() - ->title('Cloudflare: Upsert failed') - ->body('Failed to upsert record for ' . $this->name . '.' . ($this->domain->name ?? 'unknown') . '. See logs for details. Errors: ' . json_encode($result['errors'] ?? $result['body'] ?? [])) + ->title('Cloudflare: Delete failed') + ->body('Failed to delete Cloudflare record for ' . $this->name . '.' . ($this->domain->name ?? 'unknown') . '. See logs for details. Errors: ' . json_encode($result['errors'] ?? $result['body'] ?? [])) ->send(); - } } } } From 7d1b33a62916c2a9c248129d4e1caec697064d71 Mon Sep 17 00:00:00 2001 From: HarlequinSin Date: Mon, 29 Dec 2025 09:57:55 +1100 Subject: [PATCH 16/51] Remove unused Hidden component --- .../Servers/RelationManagers/SubdomainRelationManager.php | 1 - 1 file changed, 1 deletion(-) diff --git a/subdomains/src/Filament/Admin/Resources/Servers/RelationManagers/SubdomainRelationManager.php b/subdomains/src/Filament/Admin/Resources/Servers/RelationManagers/SubdomainRelationManager.php index 537fa05..ab5c570 100644 --- a/subdomains/src/Filament/Admin/Resources/Servers/RelationManagers/SubdomainRelationManager.php +++ b/subdomains/src/Filament/Admin/Resources/Servers/RelationManagers/SubdomainRelationManager.php @@ -9,7 +9,6 @@ use Filament\Actions\CreateAction; use Filament\Actions\DeleteAction; use Filament\Actions\EditAction; -use Filament\Forms\Components\Hidden; use Filament\Forms\Components\Select; use Filament\Forms\Components\TextInput; use Filament\Forms\Components\Toggle; From 910b812cd1bc86d08275042503a79d2ed3c777b7 Mon Sep 17 00:00:00 2001 From: HarlequinSin Date: Mon, 29 Dec 2025 09:59:31 +1100 Subject: [PATCH 17/51] - Added default for subdomain selection - Removed inbuilt notifications in favor of service notifications --- .../Server/Resources/Subdomains/SubdomainResource.php | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/subdomains/src/Filament/Server/Resources/Subdomains/SubdomainResource.php b/subdomains/src/Filament/Server/Resources/Subdomains/SubdomainResource.php index b7f4313..bc3ba9c 100644 --- a/subdomains/src/Filament/Server/Resources/Subdomains/SubdomainResource.php +++ b/subdomains/src/Filament/Server/Resources/Subdomains/SubdomainResource.php @@ -12,7 +12,6 @@ use Filament\Actions\DeleteAction; use Filament\Actions\EditAction; use Filament\Facades\Filament; -use Filament\Forms\Components\Hidden; use Filament\Forms\Components\Select; use Filament\Forms\Components\TextInput; use Filament\Forms\Components\Toggle; @@ -87,7 +86,8 @@ public static function table(Table $table): Table ]) ->recordActions([ EditAction::make(), - DeleteAction::make(), + DeleteAction::make() + ->successNotification(null), ]) ->toolbarActions([ CreateAction::make() @@ -98,6 +98,7 @@ public static function table(Table $table): Table ->createAnother(false) ->hiddenLabel() ->iconButton() + ->successNotification(null) ->iconSize(IconSize::ExtraLarge), ]); } @@ -116,11 +117,12 @@ public static function form(Schema $schema): Schema ->required() ->relationship('domain', 'name') ->preload() + ->default(fn () => CloudflareDomain::first()?->id ?? null) ->searchable(), Toggle::make('srv_record') ->label(trans('subdomains::strings.srv_record')) ->helperText(trans('subdomains::strings.srv_record_help')) - ->disabled(fn (callable $get) => !$get('domain_id') || empty(CloudflareDomain::find($get('domain_id'))->srv_target ?? null)) + ->disabled(false) // TODO: Dynamically disable if domain missing srv_target ->default(false), ]); } From b42370d97398c0152ccb81ef2b55d0e7706d2bc6 Mon Sep 17 00:00:00 2001 From: HarlequinSin Date: Mon, 29 Dec 2025 10:00:34 +1100 Subject: [PATCH 18/51] - Fixed erroneous success logic - Fixed Style/unused class issues --- subdomains/src/Models/CloudflareDomain.php | 23 +++++++++---------- subdomains/src/Services/CloudflareService.php | 1 - 2 files changed, 11 insertions(+), 13 deletions(-) diff --git a/subdomains/src/Models/CloudflareDomain.php b/subdomains/src/Models/CloudflareDomain.php index 134e417..4602745 100644 --- a/subdomains/src/Models/CloudflareDomain.php +++ b/subdomains/src/Models/CloudflareDomain.php @@ -2,11 +2,10 @@ namespace Boy132\Subdomains\Models; -use Illuminate\Database\Eloquent\Model; -use Illuminate\Database\Eloquent\Relations\HasMany; -use Illuminate\Support\Facades\Http; use Boy132\Subdomains\Services\CloudflareService; use Filament\Notifications\Notification; +use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\Relations\HasMany; /** * @property int $id @@ -35,16 +34,16 @@ protected static function boot(): void ->title('Failed to fetch Cloudflare Zone ID for domain: ' . $model->name) ->danger() ->send(); - } - - Notification::make() - ->title('Successfully saved domain: ' . $model->name) - ->success() - ->send(); + } else { + Notification::make() + ->title('Successfully saved domain: ' . $model->name) + ->success() + ->send(); - $model->update([ - 'cloudflare_id' => $zoneId, - ]); + $model->update([ + 'cloudflare_id' => $zoneId, + ]); + } }); } diff --git a/subdomains/src/Services/CloudflareService.php b/subdomains/src/Services/CloudflareService.php index f683714..6832418 100644 --- a/subdomains/src/Services/CloudflareService.php +++ b/subdomains/src/Services/CloudflareService.php @@ -135,7 +135,6 @@ protected function parseCloudflareHttpResponse(Response $response): array ]; } - public function deleteDnsRecord(string $zoneId, string $recordId): array { if (empty($zoneId) || empty($recordId)) { From ea4e6023d91f9a9675971c4b2c7f5c4798cf258f Mon Sep 17 00:00:00 2001 From: HarlequinSin Date: Mon, 29 Dec 2025 10:08:43 +1100 Subject: [PATCH 19/51] Removed unused classes --- subdomains/src/Models/Subdomain.php | 5 ++--- subdomains/src/Services/CloudflareService.php | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/subdomains/src/Models/Subdomain.php b/subdomains/src/Models/Subdomain.php index a78125f..e598a45 100644 --- a/subdomains/src/Models/Subdomain.php +++ b/subdomains/src/Models/Subdomain.php @@ -3,14 +3,13 @@ namespace Boy132\Subdomains\Models; use App\Models\Server; +use Boy132\Subdomains\Services\CloudflareService; +use Filament\Notifications\Notification; use Filament\Support\Contracts\HasLabel; use Illuminate\Contracts\Support\Htmlable; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; -use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Log; -use Filament\Notifications\Notification; -use Boy132\Subdomains\Services\CloudflareService; /** * @property int $id diff --git a/subdomains/src/Services/CloudflareService.php b/subdomains/src/Services/CloudflareService.php index 6832418..dcbd5dc 100644 --- a/subdomains/src/Services/CloudflareService.php +++ b/subdomains/src/Services/CloudflareService.php @@ -2,9 +2,9 @@ namespace Boy132\Subdomains\Services; +use Illuminate\Http\Client\Response; use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Log; -use Illuminate\Http\Client\Response; class CloudflareService { From 66d4384507275664b1060f9631adfae683bc237b Mon Sep 17 00:00:00 2001 From: HarlequinSin Date: Mon, 29 Dec 2025 10:22:24 +1100 Subject: [PATCH 20/51] lang support --- subdomains/lang/en/strings.php | 28 +++++++++++++ .../Subdomains/SubdomainResource.php | 3 +- subdomains/src/Models/CloudflareDomain.php | 4 +- subdomains/src/Models/Subdomain.php | 39 ++++++++++--------- subdomains/src/SubdomainsPlugin.php | 2 +- 5 files changed, 53 insertions(+), 23 deletions(-) diff --git a/subdomains/lang/en/strings.php b/subdomains/lang/en/strings.php index 3bc8625..796ef63 100644 --- a/subdomains/lang/en/strings.php +++ b/subdomains/lang/en/strings.php @@ -34,5 +34,33 @@ 'dns_action_failed' => 'Cloudflare DNS action failed', 'zone_request_failed' => 'Cloudflare zone request failed', 'zone_request_succeeded' => 'Cloudflare zone request succeeded', + + 'cloudflare_missing_zone_title' => 'Cloudflare: Missing Zone ID', + 'cloudflare_missing_zone' => 'Cloudflare zone ID is not configured for :domain. Cannot upsert DNS record for :subdomain.', + + 'cloudflare_missing_srv_port_title' => 'Cloudflare: Missing SRV Port', + 'cloudflare_missing_srv_port' => 'SRV target or port is missing for :subdomain. Cannot upsert SRV record.', + + 'cloudflare_record_updated_title' => 'Cloudflare: Record updated', + 'cloudflare_record_updated' => 'Successfully updated :subdomain to :record_type', + + 'cloudflare_srv_upsert_failed_title' => 'Cloudflare: SRV upsert failed', + 'cloudflare_srv_upsert_failed' => 'Failed to upsert SRV record for :subdomain. See logs for details. Errors: :errors', + + 'cloudflare_missing_ip_title' => 'Cloudflare: Missing IP', + 'cloudflare_missing_ip' => 'Server allocation IP is missing or invalid for :subdomain. Cannot upsert A/AAAA record.', + + 'cloudflare_upsert_failed_title' => 'Cloudflare: Upsert failed', + 'cloudflare_upsert_failed' => 'Failed to upsert record for :subdomain. See logs for details. Errors: :errors', + + 'cloudflare_delete_success_title' => 'Cloudflare: Record deleted', + 'cloudflare_delete_success' => 'Successfully deleted Cloudflare record for :subdomain.', + + 'cloudflare_delete_failed_title' => 'Cloudflare: Delete failed', + 'cloudflare_delete_failed' => 'Failed to delete Cloudflare record for :subdomain. See logs for details. Errors: :errors', + + 'cloudflare_zone_fetch_failed' => 'Failed to fetch Cloudflare Zone ID for domain: :domain', + 'cloudflare_domain_saved' => 'Successfully saved domain: :domain', + 'settings_saved' => 'Settings saved', ], ]; diff --git a/subdomains/src/Filament/Server/Resources/Subdomains/SubdomainResource.php b/subdomains/src/Filament/Server/Resources/Subdomains/SubdomainResource.php index bc3ba9c..3b8c25f 100644 --- a/subdomains/src/Filament/Server/Resources/Subdomains/SubdomainResource.php +++ b/subdomains/src/Filament/Server/Resources/Subdomains/SubdomainResource.php @@ -81,11 +81,12 @@ public static function table(Table $table): Table ->state(fn (Subdomain $subdomain) => $subdomain->getLabel()), ToggleColumn::make('srv_record') ->label(trans('subdomains::strings.srv_record')) - ->tooltip(fn (Subdomain $record) => $record->domain && $record->domain->srv_target ? trans('subdomains::strings.srv_record_help') : trans('subdomains::strings.errors.srv_target_missing')) + ->tooltip(fn (Subdomain $record) => $record->domain && $record->domain->srv_target ? trans('subdomains::strings.srv_record_help') : trans('subdomains::strings.srv_target_missing')) ->disabled(fn (Subdomain $record) => !$record->domain || empty($record->domain->srv_target)), ]) ->recordActions([ EditAction::make(), + ->successNotification(null), DeleteAction::make() ->successNotification(null), ]) diff --git a/subdomains/src/Models/CloudflareDomain.php b/subdomains/src/Models/CloudflareDomain.php index 4602745..1dbd87c 100644 --- a/subdomains/src/Models/CloudflareDomain.php +++ b/subdomains/src/Models/CloudflareDomain.php @@ -31,12 +31,12 @@ protected static function boot(): void $zoneId = $service->getZoneId($model->name); if (!$zoneId) { Notification::make() - ->title('Failed to fetch Cloudflare Zone ID for domain: ' . $model->name) + ->title(trans('subdomains::strings.notifications.cloudflare_zone_fetch_failed', ['domain' => $model->name])) ->danger() ->send(); } else { Notification::make() - ->title('Successfully saved domain: ' . $model->name) + ->title(trans('subdomains::strings.notifications.cloudflare_domain_saved', ['domain' => $model->name])) ->success() ->send(); diff --git a/subdomains/src/Models/Subdomain.php b/subdomains/src/Models/Subdomain.php index e598a45..53f21d6 100644 --- a/subdomains/src/Models/Subdomain.php +++ b/subdomains/src/Models/Subdomain.php @@ -99,8 +99,8 @@ protected function upsertOnCloudflare(): void Notification::make() ->danger() - ->title('Cloudflare: Missing Zone ID') - ->body(sprintf('Cloudflare zone ID is not configured for %s. Cannot upsert DNS record for %s.%s.', $this->domain->name ?? 'unknown', $this->name, $this->domain->name ?? 'unknown')) + ->title(trans('subdomains::strings.notifications.cloudflare_missing_zone_title')) + ->body(trans('subdomains::strings.notifications.cloudflare_missing_zone', ['domain' => $this->domain->name ?? 'unknown', 'subdomain' => $this->name . '.' . ($this->domain->name ?? 'unknown')])) ->send(); return; @@ -115,8 +115,8 @@ protected function upsertOnCloudflare(): void Notification::make() ->danger() - ->title('Cloudflare: Missing SRV Port') - ->body(sprintf('SRV target or port is missing for %s.%s. Cannot upsert SRV record.', $this->name, $this->domain->name ?? 'unknown')) + ->title(trans('subdomains::strings.notifications.cloudflare_missing_srv_port_title')) + ->body(trans('subdomains::strings.notifications.cloudflare_missing_srv_port', ['subdomain' => $this->name . '.' . ($this->domain->name ?? 'unknown')])) ->send(); return; @@ -131,16 +131,16 @@ protected function upsertOnCloudflare(): void Notification::make() ->success() - ->title('Cloudflare: Record updated') - ->body('Successfully updated ' . $this->name . '.' . ($this->domain->name ?? 'unknown') . ' to '. $this->record_type) + ->title(trans('subdomains::strings.notifications.cloudflare_record_updated_title')) + ->body(trans('subdomains::strings.notifications.cloudflare_record_updated', ['subdomain' => $this->name . '.' . ($this->domain->name ?? 'unknown'), 'record_type' => $this->record_type])) ->send(); } else { Log::error('Failed to upsert SRV record on Cloudflare for Subdomain ID ' . $this->id, ['result' => $result]); Notification::make() ->danger() - ->title('Cloudflare: SRV upsert failed') - ->body('Failed to upsert SRV record for ' . $this->name . '.' . ($this->domain->name ?? 'unknown') . '. See logs for details. Errors: ' . json_encode($result['errors'] ?? $result['body'] ?? [])) + ->title(trans('subdomains::strings.notifications.cloudflare_srv_upsert_failed_title')) + ->body(trans('subdomains::strings.notifications.cloudflare_srv_upsert_failed', ['subdomain' => $this->name . '.' . ($this->domain->name ?? 'unknown'), 'errors' => json_encode($result['errors'] ?? $result['body'] ?? [])])) ->send(); } @@ -153,8 +153,8 @@ protected function upsertOnCloudflare(): void Notification::make() ->danger() - ->title('Cloudflare: Missing IP') - ->body(sprintf('Server allocation IP is missing or invalid for %s.%s. Cannot upsert A/AAAA record.', $this->name, $this->domain->name ?? 'unknown')) + ->title(trans('subdomains::strings.notifications.cloudflare_missing_ip_title')) + ->body(trans('subdomains::strings.notifications.cloudflare_missing_ip', ['subdomain' => $this->name . '.' . ($this->domain->name ?? 'unknown')])) ->send(); return; @@ -169,8 +169,8 @@ protected function upsertOnCloudflare(): void Notification::make() ->success() - ->title('Cloudflare: Record updated') - ->body('Successfully updated ' . $this->name . '.' . ($this->domain->name ?? 'unknown') . ' to '. $this->record_type) + ->title(trans('subdomains::strings.notifications.cloudflare_record_updated_title')) + ->body(trans('subdomains::strings.notifications.cloudflare_record_updated', ['subdomain' => $this->name . '.' . ($this->domain->name ?? 'unknown'), 'record_type' => $this->record_type])) ->send(); } else { Log::error('Failed to upsert record on Cloudflare for Subdomain ID ' . $this->id, ['result' => $result]); @@ -180,8 +180,8 @@ protected function upsertOnCloudflare(): void Notification::make() ->danger() - ->title('Cloudflare: Upsert failed') - ->body('Failed to upsert record for ' . $sub . '. See logs for details. Errors: ' . json_encode($result['errors'] ?? $result['body'] ?? [])) + ->title(trans('subdomains::strings.notifications.cloudflare_upsert_failed_title')) + ->body(trans('subdomains::strings.notifications.cloudflare_upsert_failed', ['subdomain' => $sub, 'errors' => json_encode($result['errors'] ?? $result['body'] ?? [])])) ->send(); } } @@ -195,16 +195,17 @@ protected function deleteOnCloudflare(): void if (!empty($result['success'])) { Notification::make() - ->danger() - ->title('Cloudflare: Delete successed') - ->body('Successfully deleted Cloudflare record for ' . $this->name . '.' . ($this->domain->name ?? 'unknown') . '.') + ->success() + ->title(trans('subdomains::strings.notifications.cloudflare_delete_success_title')) + ->body(trans('subdomains::strings.notifications.cloudflare_delete_success', ['subdomain' => $this->name . '.' . ($this->domain->name ?? 'unknown')])) ->send(); + return; } Notification::make() ->danger() - ->title('Cloudflare: Delete failed') - ->body('Failed to delete Cloudflare record for ' . $this->name . '.' . ($this->domain->name ?? 'unknown') . '. See logs for details. Errors: ' . json_encode($result['errors'] ?? $result['body'] ?? [])) + ->title(trans('subdomains::strings.notifications.cloudflare_delete_failed_title')) + ->body(trans('subdomains::strings.notifications.cloudflare_delete_failed', ['subdomain' => $this->name . '.' . ($this->domain->name ?? 'unknown'), 'errors' => json_encode($result['errors'] ?? $result['body'] ?? [])])) ->send(); } } diff --git a/subdomains/src/SubdomainsPlugin.php b/subdomains/src/SubdomainsPlugin.php index 17e7164..655a465 100644 --- a/subdomains/src/SubdomainsPlugin.php +++ b/subdomains/src/SubdomainsPlugin.php @@ -47,7 +47,7 @@ public function saveSettings(array $data): void ]); Notification::make() - ->title('Settings saved') + ->title(trans('subdomains::strings.notifications.settings_saved')) ->success() ->send(); } From f7b98d3a412e9694fd8091fd6ec4c9058ff55c85 Mon Sep 17 00:00:00 2001 From: HarlequinSin Date: Mon, 29 Dec 2025 10:27:05 +1100 Subject: [PATCH 21/51] corrected notification log --- subdomains/src/Models/Subdomain.php | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/subdomains/src/Models/Subdomain.php b/subdomains/src/Models/Subdomain.php index 53f21d6..9dd6d93 100644 --- a/subdomains/src/Models/Subdomain.php +++ b/subdomains/src/Models/Subdomain.php @@ -200,13 +200,13 @@ protected function deleteOnCloudflare(): void ->body(trans('subdomains::strings.notifications.cloudflare_delete_success', ['subdomain' => $this->name . '.' . ($this->domain->name ?? 'unknown')])) ->send(); return; + } else { + Notification::make() + ->danger() + ->title(trans('subdomains::strings.notifications.cloudflare_delete_failed_title')) + ->body(trans('subdomains::strings.notifications.cloudflare_delete_failed', ['subdomain' => $this->name . '.' . ($this->domain->name ?? 'unknown'), 'errors' => json_encode($result['errors'] ?? $result['body'] ?? [])])) + ->send(); } - - Notification::make() - ->danger() - ->title(trans('subdomains::strings.notifications.cloudflare_delete_failed_title')) - ->body(trans('subdomains::strings.notifications.cloudflare_delete_failed', ['subdomain' => $this->name . '.' . ($this->domain->name ?? 'unknown'), 'errors' => json_encode($result['errors'] ?? $result['body'] ?? [])])) - ->send(); } } } From 9804937fc3e8c915c6447dbae252b0d59693b537 Mon Sep 17 00:00:00 2001 From: HarlequinSin Date: Mon, 29 Dec 2025 10:27:24 +1100 Subject: [PATCH 22/51] Fixed http success logic --- subdomains/src/Services/CloudflareService.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/subdomains/src/Services/CloudflareService.php b/subdomains/src/Services/CloudflareService.php index dcbd5dc..b2cd73b 100644 --- a/subdomains/src/Services/CloudflareService.php +++ b/subdomains/src/Services/CloudflareService.php @@ -124,7 +124,7 @@ protected function parseCloudflareHttpResponse(Response $response): array $status = $response->status(); $body = $response->json() ?? []; - $success = $response->successful() && (!empty($body['success']) || !empty($body['result'])); + $success = $response->successful() && ($body['success'] === true || count($body['result']) > 0); return [ 'success' => $success, From d004ddaf94af9066c401d931a371cb4964814c06 Mon Sep 17 00:00:00 2001 From: HarlequinSin Date: Mon, 29 Dec 2025 10:32:29 +1100 Subject: [PATCH 23/51] fixed style issues and syntax error --- .../Server/Resources/Subdomains/SubdomainResource.php | 2 +- subdomains/src/Models/Subdomain.php | 5 +++-- subdomains/src/Services/CloudflareService.php | 9 +-------- 3 files changed, 5 insertions(+), 11 deletions(-) diff --git a/subdomains/src/Filament/Server/Resources/Subdomains/SubdomainResource.php b/subdomains/src/Filament/Server/Resources/Subdomains/SubdomainResource.php index 3b8c25f..b44ffbd 100644 --- a/subdomains/src/Filament/Server/Resources/Subdomains/SubdomainResource.php +++ b/subdomains/src/Filament/Server/Resources/Subdomains/SubdomainResource.php @@ -85,7 +85,7 @@ public static function table(Table $table): Table ->disabled(fn (Subdomain $record) => !$record->domain || empty($record->domain->srv_target)), ]) ->recordActions([ - EditAction::make(), + EditAction::make() ->successNotification(null), DeleteAction::make() ->successNotification(null), diff --git a/subdomains/src/Models/Subdomain.php b/subdomains/src/Models/Subdomain.php index 9dd6d93..b243255 100644 --- a/subdomains/src/Models/Subdomain.php +++ b/subdomains/src/Models/Subdomain.php @@ -128,7 +128,7 @@ protected function upsertOnCloudflare(): void if ($this->cloudflare_id !== $result['id']) { $this->updateQuietly(['cloudflare_id' => $result['id']]); } - + Notification::make() ->success() ->title(trans('subdomains::strings.notifications.cloudflare_record_updated_title')) @@ -199,7 +199,6 @@ protected function deleteOnCloudflare(): void ->title(trans('subdomains::strings.notifications.cloudflare_delete_success_title')) ->body(trans('subdomains::strings.notifications.cloudflare_delete_success', ['subdomain' => $this->name . '.' . ($this->domain->name ?? 'unknown')])) ->send(); - return; } else { Notification::make() ->danger() @@ -207,6 +206,8 @@ protected function deleteOnCloudflare(): void ->body(trans('subdomains::strings.notifications.cloudflare_delete_failed', ['subdomain' => $this->name . '.' . ($this->domain->name ?? 'unknown'), 'errors' => json_encode($result['errors'] ?? $result['body'] ?? [])])) ->send(); } + + return; } } } diff --git a/subdomains/src/Services/CloudflareService.php b/subdomains/src/Services/CloudflareService.php index b2cd73b..11f57dc 100644 --- a/subdomains/src/Services/CloudflareService.php +++ b/subdomains/src/Services/CloudflareService.php @@ -38,14 +38,7 @@ public function getZoneId(string $domainName): ?string return null; } - public function upsertDnsRecord( - string $zoneId, - string $name, - string $recordType, - string $target, - ?string $recordId = null, - ?int $port = null, - ): array + public function upsertDnsRecord(string $zoneId, string $name, string $recordType, string $target, ?string $recordId = null, ?int $port = null): array { if (empty($zoneId) || empty($name) || empty($recordType)) { Log::error('Cloudflare upsertDnsRecord missing required parameters', ['zone' => $zoneId, 'name' => $name, 'type' => $recordType]); From a4d9827007f7619bc6f4f342b154c5ecdd37f745 Mon Sep 17 00:00:00 2001 From: HarlequinSin Date: Mon, 29 Dec 2025 10:46:58 +1100 Subject: [PATCH 24/51] fixing blank_line_before_statement --- subdomains/src/Services/CloudflareService.php | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/subdomains/src/Services/CloudflareService.php b/subdomains/src/Services/CloudflareService.php index 11f57dc..b0525e5 100644 --- a/subdomains/src/Services/CloudflareService.php +++ b/subdomains/src/Services/CloudflareService.php @@ -12,6 +12,7 @@ public function getZoneId(string $domainName): ?string { if (empty($domainName)) { Log::error('Cloudflare getZoneId called with empty domain name', ['domain' => $domainName]); + return null; } @@ -21,6 +22,7 @@ public function getZoneId(string $domainName): ?string ]); } catch (\Throwable $e) { Log::error('Cloudflare getZoneId request failed: ' . $e->getMessage(), ['domain' => $domainName]); + return null; } @@ -42,6 +44,7 @@ public function upsertDnsRecord(string $zoneId, string $name, string $recordType { if (empty($zoneId) || empty($name) || empty($recordType)) { Log::error('Cloudflare upsertDnsRecord missing required parameters', ['zone' => $zoneId, 'name' => $name, 'type' => $recordType]); + return ['success' => false, 'id' => null, 'errors' => ['missing_parameters' => true], 'status' => 0, 'body' => null]; } @@ -56,6 +59,7 @@ public function upsertDnsRecord(string $zoneId, string $name, string $recordType if ($recordType === 'SRV') { if (empty($port) || empty($target)) { Log::error('Cloudflare upsert missing SRV target or port', ['zone' => $zoneId, 'name' => $name, 'type' => $recordType]); + return ['success' => false, 'id' => null, 'errors' => ['missing_srv_target_or_port' => true], 'status' => 0, 'body' => null]; } @@ -94,6 +98,7 @@ public function upsertDnsRecord(string $zoneId, string $name, string $recordType } Log::error('Cloudflare update failed', ['zone' => $zoneId, 'recordId' => $recordId, 'response' => $parsed]); + return $parsed; } @@ -105,9 +110,11 @@ public function upsertDnsRecord(string $zoneId, string $name, string $recordType } Log::error('Cloudflare create failed', ['zone' => $zoneId, 'payload' => $payload, 'response' => $parsed]); + return $parsed; } catch (\Throwable $e) { Log::error('Cloudflare upsert exception: ' . $e->getMessage(), ['zone' => $zoneId, 'payload' => $payload]); + return ['success' => false, 'id' => null, 'errors' => ['exception' => $e->getMessage()], 'status' => 0, 'body' => null]; } } @@ -144,9 +151,11 @@ public function deleteDnsRecord(string $zoneId, string $recordId): array } Log::error('Cloudflare delete failed', ['zone' => $zoneId, 'id' => $recordId, 'response' => $parsed]); + return $parsed; } catch (\Throwable $e) { Log::error('Cloudflare delete exception: ' . $e->getMessage(), ['zone' => $zoneId, 'id' => $recordId]); + return ['success' => false, 'errors' => ['exception' => $e->getMessage()], 'status' => 0, 'body' => null]; } } From c4654fd2a273181e86d22dfeff32327d2ac467bd Mon Sep 17 00:00:00 2001 From: HarlequinSin Date: Mon, 29 Dec 2025 10:54:41 +1100 Subject: [PATCH 25/51] append version to 1.1.0 --- subdomains/plugin.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/subdomains/plugin.json b/subdomains/plugin.json index 7ff7cbc..23e3ee6 100644 --- a/subdomains/plugin.json +++ b/subdomains/plugin.json @@ -2,7 +2,7 @@ "id": "subdomains", "name": "Subdomains", "author": "Boy132", - "version": "1.0.0", + "version": "1.1.0", "description": "Allows users to create subdomains for their servers", "category": "plugin", "url": "https://github.com/pelican-dev/plugins/tree/main/subdomains", From 348099d16c2fcce30010e258c3bf526884398630 Mon Sep 17 00:00:00 2001 From: HarlequinSin Date: Mon, 29 Dec 2025 12:53:39 +1100 Subject: [PATCH 26/51] Add notification for missing SRV target in Cloudflare integration --- subdomains/lang/en/strings.php | 3 +++ subdomains/src/Models/Subdomain.php | 12 ++++++++++++ 2 files changed, 15 insertions(+) diff --git a/subdomains/lang/en/strings.php b/subdomains/lang/en/strings.php index 796ef63..462d767 100644 --- a/subdomains/lang/en/strings.php +++ b/subdomains/lang/en/strings.php @@ -41,6 +41,9 @@ 'cloudflare_missing_srv_port_title' => 'Cloudflare: Missing SRV Port', 'cloudflare_missing_srv_port' => 'SRV target or port is missing for :subdomain. Cannot upsert SRV record.', + 'cloudflare_missing_srv_target_title' => 'Cloudflare: Missing SRV Target', + 'cloudflare_missing_srv_target' => 'SRV target is missing for :subdomain. Cannot upsert SRV record.', + 'cloudflare_record_updated_title' => 'Cloudflare: Record updated', 'cloudflare_record_updated' => 'Successfully updated :subdomain to :record_type', diff --git a/subdomains/src/Models/Subdomain.php b/subdomains/src/Models/Subdomain.php index b243255..78e1c0c 100644 --- a/subdomains/src/Models/Subdomain.php +++ b/subdomains/src/Models/Subdomain.php @@ -122,6 +122,18 @@ protected function upsertOnCloudflare(): void return; } + if (empty($this->domain->srv_target)) { + Log::warning('Domain missing SRV target for SRV record', ['domain_id' => $this->domain_id]); + + Notification::make() + ->danger() + ->title(trans('subdomains::strings.notifications.cloudflare_missing_srv_target_title')) + ->body(trans('subdomains::strings.notifications.cloudflare_missing_srv_target', ['subdomain' => $this->name . '.' . ($this->domain->name ?? 'unknown')])) + ->send(); + + return; + } + $result = $service->upsertDnsRecord($zoneId, $this->name, 'SRV', $this->domain->srv_target, $this->cloudflare_id ?? null, $port); if ($result['success'] && !empty($result['id'])) { From d64612d694daf2c66591a73950b8fab7bf15faeb Mon Sep 17 00:00:00 2001 From: HarlequinSin Date: Mon, 29 Dec 2025 13:03:20 +1100 Subject: [PATCH 27/51] minor wording updates --- subdomains/lang/en/strings.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/subdomains/lang/en/strings.php b/subdomains/lang/en/strings.php index 462d767..5ae1ac4 100644 --- a/subdomains/lang/en/strings.php +++ b/subdomains/lang/en/strings.php @@ -39,13 +39,13 @@ 'cloudflare_missing_zone' => 'Cloudflare zone ID is not configured for :domain. Cannot upsert DNS record for :subdomain.', 'cloudflare_missing_srv_port_title' => 'Cloudflare: Missing SRV Port', - 'cloudflare_missing_srv_port' => 'SRV target or port is missing for :subdomain. Cannot upsert SRV record.', + 'cloudflare_missing_srv_port' => 'SRV port is missing for :subdomain. Cannot upsert SRV record.', 'cloudflare_missing_srv_target_title' => 'Cloudflare: Missing SRV Target', 'cloudflare_missing_srv_target' => 'SRV target is missing for :subdomain. Cannot upsert SRV record.', 'cloudflare_record_updated_title' => 'Cloudflare: Record updated', - 'cloudflare_record_updated' => 'Successfully updated :subdomain to :record_type', + 'cloudflare_record_updated' => 'Successfully updated :subdomain record to :record_type', 'cloudflare_srv_upsert_failed_title' => 'Cloudflare: SRV upsert failed', 'cloudflare_srv_upsert_failed' => 'Failed to upsert SRV record for :subdomain. See logs for details. Errors: :errors', From 5bfd8dfd25ed1a05f63d6ff5b3a9495b3e236719 Mon Sep 17 00:00:00 2001 From: HarlequinSin Date: Mon, 29 Dec 2025 13:20:39 +1100 Subject: [PATCH 28/51] Dynamically disable SRV record toggle if domain is missing srv_target --- .../Filament/Server/Resources/Subdomains/SubdomainResource.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/subdomains/src/Filament/Server/Resources/Subdomains/SubdomainResource.php b/subdomains/src/Filament/Server/Resources/Subdomains/SubdomainResource.php index b44ffbd..c2b65ee 100644 --- a/subdomains/src/Filament/Server/Resources/Subdomains/SubdomainResource.php +++ b/subdomains/src/Filament/Server/Resources/Subdomains/SubdomainResource.php @@ -123,7 +123,7 @@ public static function form(Schema $schema): Schema Toggle::make('srv_record') ->label(trans('subdomains::strings.srv_record')) ->helperText(trans('subdomains::strings.srv_record_help')) - ->disabled(false) // TODO: Dynamically disable if domain missing srv_target + ->disabled(fn (callable $get, ?Subdomain $record = null) => (empty($get('domain_id')) || empty(CloudflareDomain::find($get('domain_id'))?->srv_target))) ->default(false), ]); } From e781f6a5bce26777680759e71c9f49f01fe2657b Mon Sep 17 00:00:00 2001 From: HarlequinSin Date: Mon, 29 Dec 2025 13:29:36 +1100 Subject: [PATCH 29/51] lang cleanup, removed redundent strings --- subdomains/lang/en/strings.php | 23 +++++++---------------- subdomains/src/Models/Subdomain.php | 4 ++-- 2 files changed, 9 insertions(+), 18 deletions(-) diff --git a/subdomains/lang/en/strings.php b/subdomains/lang/en/strings.php index 5ae1ac4..494b760 100644 --- a/subdomains/lang/en/strings.php +++ b/subdomains/lang/en/strings.php @@ -28,33 +28,23 @@ 'api_token_help' => 'The token needs to have read permissions for Zone.Zone and write for Zone.Dns. For better security you can also set the "Zone Resources" to exclude certain domains and add the panel ip to the "Client IP Adress Filtering".', 'notifications' => [ - 'dns_created' => 'DNS record created on Cloudflare', - 'dns_updated' => 'DNS record updated on Cloudflare', - 'dns_deleted' => 'DNS record deleted from Cloudflare', - 'dns_action_failed' => 'Cloudflare DNS action failed', - 'zone_request_failed' => 'Cloudflare zone request failed', - 'zone_request_succeeded' => 'Cloudflare zone request succeeded', - 'cloudflare_missing_zone_title' => 'Cloudflare: Missing Zone ID', - 'cloudflare_missing_zone' => 'Cloudflare zone ID is not configured for :domain. Cannot upsert DNS record for :subdomain.', + 'cloudflare_missing_zone' => 'Cloudflare zone ID is not configured for :domain. Cannot save DNS record for :subdomain.', 'cloudflare_missing_srv_port_title' => 'Cloudflare: Missing SRV Port', - 'cloudflare_missing_srv_port' => 'SRV port is missing for :subdomain. Cannot upsert SRV record.', + 'cloudflare_missing_srv_port' => 'SRV port is missing for :subdomain. Cannot save SRV record.', 'cloudflare_missing_srv_target_title' => 'Cloudflare: Missing SRV Target', - 'cloudflare_missing_srv_target' => 'SRV target is missing for :subdomain. Cannot upsert SRV record.', + 'cloudflare_missing_srv_target' => 'SRV target is missing for :subdomain. Cannot save SRV record.', 'cloudflare_record_updated_title' => 'Cloudflare: Record updated', 'cloudflare_record_updated' => 'Successfully updated :subdomain record to :record_type', - 'cloudflare_srv_upsert_failed_title' => 'Cloudflare: SRV upsert failed', - 'cloudflare_srv_upsert_failed' => 'Failed to upsert SRV record for :subdomain. See logs for details. Errors: :errors', - 'cloudflare_missing_ip_title' => 'Cloudflare: Missing IP', - 'cloudflare_missing_ip' => 'Server allocation IP is missing or invalid for :subdomain. Cannot upsert A/AAAA record.', + 'cloudflare_missing_ip' => 'Server allocation IP is missing or invalid for :subdomain. Cannot save A/AAAA record.', - 'cloudflare_upsert_failed_title' => 'Cloudflare: Upsert failed', - 'cloudflare_upsert_failed' => 'Failed to upsert record for :subdomain. See logs for details. Errors: :errors', + 'cloudflare_upsert_failed_title' => 'Cloudflare: Save failed', + 'cloudflare_upsert_failed' => 'Failed to save record for :subdomain. See logs for details. Errors: :errors', 'cloudflare_delete_success_title' => 'Cloudflare: Record deleted', 'cloudflare_delete_success' => 'Successfully deleted Cloudflare record for :subdomain.', @@ -64,6 +54,7 @@ 'cloudflare_zone_fetch_failed' => 'Failed to fetch Cloudflare Zone ID for domain: :domain', 'cloudflare_domain_saved' => 'Successfully saved domain: :domain', + 'settings_saved' => 'Settings saved', ], ]; diff --git a/subdomains/src/Models/Subdomain.php b/subdomains/src/Models/Subdomain.php index 78e1c0c..f6617fa 100644 --- a/subdomains/src/Models/Subdomain.php +++ b/subdomains/src/Models/Subdomain.php @@ -151,8 +151,8 @@ protected function upsertOnCloudflare(): void Notification::make() ->danger() - ->title(trans('subdomains::strings.notifications.cloudflare_srv_upsert_failed_title')) - ->body(trans('subdomains::strings.notifications.cloudflare_srv_upsert_failed', ['subdomain' => $this->name . '.' . ($this->domain->name ?? 'unknown'), 'errors' => json_encode($result['errors'] ?? $result['body'] ?? [])])) + ->title(trans('subdomains::strings.notifications.cloudflare_upsert_failed_title')) + ->body(trans('subdomains::strings.notifications.cloudflare_upsert_failed', ['subdomain' => $this->name . '.' . ($this->domain->name ?? 'unknown'), 'errors' => json_encode($result['errors'] ?? $result['body'] ?? [])])) ->send(); } From 8a7e932c204dd83f298f63f5e12da675ec9b538f Mon Sep 17 00:00:00 2001 From: HarlequinSin Date: Mon, 29 Dec 2025 13:39:52 +1100 Subject: [PATCH 30/51] Make SRV record toggle reactive --- .../Filament/Server/Resources/Subdomains/SubdomainResource.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/subdomains/src/Filament/Server/Resources/Subdomains/SubdomainResource.php b/subdomains/src/Filament/Server/Resources/Subdomains/SubdomainResource.php index c2b65ee..1d9fe3b 100644 --- a/subdomains/src/Filament/Server/Resources/Subdomains/SubdomainResource.php +++ b/subdomains/src/Filament/Server/Resources/Subdomains/SubdomainResource.php @@ -123,7 +123,8 @@ public static function form(Schema $schema): Schema Toggle::make('srv_record') ->label(trans('subdomains::strings.srv_record')) ->helperText(trans('subdomains::strings.srv_record_help')) - ->disabled(fn (callable $get, ?Subdomain $record = null) => (empty($get('domain_id')) || empty(CloudflareDomain::find($get('domain_id'))?->srv_target))) + ->reactive() + ->disabled(fn (callable $get) => (empty($get('domain_id')) || empty(CloudflareDomain::find($get('domain_id'))?->srv_target))) ->default(false), ]); } From 23c4e399a1a71c5417411c0d3b322fc7015cfcce Mon Sep 17 00:00:00 2001 From: HarlequinSin Date: Mon, 29 Dec 2025 13:40:14 +1100 Subject: [PATCH 31/51] Fix capitalization in Cloudflare notification titles --- subdomains/lang/en/strings.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/subdomains/lang/en/strings.php b/subdomains/lang/en/strings.php index 494b760..3b8f387 100644 --- a/subdomains/lang/en/strings.php +++ b/subdomains/lang/en/strings.php @@ -37,24 +37,24 @@ 'cloudflare_missing_srv_target_title' => 'Cloudflare: Missing SRV Target', 'cloudflare_missing_srv_target' => 'SRV target is missing for :subdomain. Cannot save SRV record.', - 'cloudflare_record_updated_title' => 'Cloudflare: Record updated', + 'cloudflare_record_updated_title' => 'Cloudflare: Record Updated', 'cloudflare_record_updated' => 'Successfully updated :subdomain record to :record_type', 'cloudflare_missing_ip_title' => 'Cloudflare: Missing IP', 'cloudflare_missing_ip' => 'Server allocation IP is missing or invalid for :subdomain. Cannot save A/AAAA record.', - 'cloudflare_upsert_failed_title' => 'Cloudflare: Save failed', + 'cloudflare_upsert_failed_title' => 'Cloudflare: Save Failed', 'cloudflare_upsert_failed' => 'Failed to save record for :subdomain. See logs for details. Errors: :errors', - 'cloudflare_delete_success_title' => 'Cloudflare: Record deleted', + 'cloudflare_delete_success_title' => 'Cloudflare: Record Deleted', 'cloudflare_delete_success' => 'Successfully deleted Cloudflare record for :subdomain.', - 'cloudflare_delete_failed_title' => 'Cloudflare: Delete failed', + 'cloudflare_delete_failed_title' => 'Cloudflare: Delete Failed', 'cloudflare_delete_failed' => 'Failed to delete Cloudflare record for :subdomain. See logs for details. Errors: :errors', 'cloudflare_zone_fetch_failed' => 'Failed to fetch Cloudflare Zone ID for domain: :domain', 'cloudflare_domain_saved' => 'Successfully saved domain: :domain', - + 'settings_saved' => 'Settings saved', ], ]; From f31a101c26a174d7814522114005cca0d38b9749 Mon Sep 17 00:00:00 2001 From: HarlequinSin Date: Mon, 29 Dec 2025 13:46:01 +1100 Subject: [PATCH 32/51] clean up --- subdomains/src/Models/Subdomain.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/subdomains/src/Models/Subdomain.php b/subdomains/src/Models/Subdomain.php index f6617fa..7288429 100644 --- a/subdomains/src/Models/Subdomain.php +++ b/subdomains/src/Models/Subdomain.php @@ -134,7 +134,7 @@ protected function upsertOnCloudflare(): void return; } - $result = $service->upsertDnsRecord($zoneId, $this->name, 'SRV', $this->domain->srv_target, $this->cloudflare_id ?? null, $port); + $result = $service->upsertDnsRecord($zoneId, $this->name, 'SRV', $this->domain->srv_target, $this->cloudflare_id, $port); if ($result['success'] && !empty($result['id'])) { if ($this->cloudflare_id !== $result['id']) { @@ -172,7 +172,7 @@ protected function upsertOnCloudflare(): void return; } - $result = $service->upsertDnsRecord($zoneId, $this->name, $this->record_type, $this->server->allocation->ip, $this->cloudflare_id ?? null, null); + $result = $service->upsertDnsRecord($zoneId, $this->name, $this->record_type, $this->server->allocation->ip, $this->cloudflare_id, null); if ($result['success'] && !empty($result['id'])) { if ($this->cloudflare_id !== $result['id']) { From 6b316869ed0a690532ed0759d856f5fd441a9f4b Mon Sep 17 00:00:00 2001 From: HarlequinSin <73203025+HarlequinSin@users.noreply.github.com> Date: Thu, 1 Jan 2026 19:49:34 +1100 Subject: [PATCH 33/51] migration to move srv_target from cloudflare domains to nodes --- .../005_add_srv_target_to_nodes.php | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 subdomains/database/migrations/005_add_srv_target_to_nodes.php diff --git a/subdomains/database/migrations/005_add_srv_target_to_nodes.php b/subdomains/database/migrations/005_add_srv_target_to_nodes.php new file mode 100644 index 0000000..cab88da --- /dev/null +++ b/subdomains/database/migrations/005_add_srv_target_to_nodes.php @@ -0,0 +1,30 @@ +dropColumn('srv_target'); + }); + + Schema::table('nodes', function (Blueprint $table) { + $table->string('srv_target')->nullable()->after('fqdn'); + }); + } + + public function down(): void + { + Schema::table('cloudflare_domains', function (Blueprint $table) { + $table->string('srv_target')->nullable()->after('cloudflare_id'); + }); + + Schema::table('nodes', function (Blueprint $table) { + $table->dropColumn('srv_target'); + }); + } +}; From d2c0e18c4af160c9d008c6278a3b2b9448aa1f7f Mon Sep 17 00:00:00 2001 From: HarlequinSin Date: Thu, 1 Jan 2026 20:02:33 +1100 Subject: [PATCH 34/51] Changing srv_target from domains to node --- .../Filament/Admin/Nodes/Pages/EditNode.php | 792 ++++++++++++++++++ .../CloudflareDomainResource.php | 7 - .../Subdomains/SubdomainResource.php | 14 +- subdomains/src/Models/CloudflareDomain.php | 1 - .../Providers/SubdomainsPluginProvider.php | 4 +- 5 files changed, 802 insertions(+), 16 deletions(-) create mode 100644 subdomains/src/Filament/Admin/Nodes/Pages/EditNode.php diff --git a/subdomains/src/Filament/Admin/Nodes/Pages/EditNode.php b/subdomains/src/Filament/Admin/Nodes/Pages/EditNode.php new file mode 100644 index 0000000..c819888 --- /dev/null +++ b/subdomains/src/Filament/Admin/Nodes/Pages/EditNode.php @@ -0,0 +1,792 @@ +components([ + Tabs::make('Tabs') + ->columns([ + 'default' => 2, + 'sm' => 3, + 'md' => 3, + 'lg' => 4, + ]) + ->persistTabInQueryString() + ->columnSpanFull() + ->tabs([ + Tab::make('overview') + ->label(trans('admin/node.tabs.overview')) + ->icon('tabler-chart-area-line-filled') + ->columns([ + 'default' => 4, + 'sm' => 2, + 'md' => 4, + 'lg' => 4, + ]) + ->schema([ + Fieldset::make() + ->label(trans('admin/node.node_info')) + ->columns(4) + ->columnSpanFull() + ->schema([ + TextEntry::make('wings_version') + ->label(trans('admin/node.wings_version')) + ->state(fn (Node $node, SoftwareVersionService $versionService) => ($node->systemInformation()['version'] ?? trans('admin/node.unknown')) . ' ' . trans('admin/node.latest', ['version' => $versionService->latestWingsVersion()])), + TextEntry::make('cpu_threads') + ->label(trans('admin/node.cpu_threads')) + ->state(fn (Node $node) => $node->systemInformation()['cpu_count'] ?? 0), + TextEntry::make('architecture') + ->label(trans('admin/node.architecture')) + ->state(fn (Node $node) => $node->systemInformation()['architecture'] ?? trans('admin/node.unknown')), + TextEntry::make('kernel') + ->label(trans('admin/node.kernel')) + ->state(fn (Node $node) => $node->systemInformation()['kernel_version'] ?? trans('admin/node.unknown')), + ]), + View::make('filament.components.node-cpu-chart') + ->columnSpan([ + 'default' => 4, + 'sm' => 1, + 'md' => 2, + 'lg' => 2, + ]), + View::make('filament.components.node-memory-chart') + ->columnSpan([ + 'default' => 4, + 'sm' => 1, + 'md' => 2, + 'lg' => 2, + ]), + View::make('filament.components.node-storage-chart') + ->columnSpanFull(), + ]), + Tab::make('basic_settings') + ->label(trans('admin/node.tabs.basic_settings')) + ->icon('tabler-server') + ->schema([ + TextInput::make('fqdn') + ->columnSpan(2) + ->required() + ->autofocus() + ->live(debounce: 1500) + ->rules(Node::getRulesForField('fqdn')) + ->prohibited(fn ($state) => is_ip($state) && request()->isSecure()) + ->label(fn ($state) => is_ip($state) ? trans('admin/node.ip_address') : trans('admin/node.domain')) + ->placeholder(fn ($state) => is_ip($state) ? '192.168.1.1' : 'node.example.com') + ->helperText(function ($state) { + if (is_ip($state)) { + if (request()->isSecure()) { + return trans('admin/node.fqdn_help'); + } + + return ''; + } + + return trans('admin/node.error'); + }) + ->hintColor('danger') + ->hint(function ($state) { + if (is_ip($state) && request()->isSecure()) { + return trans('admin/node.ssl_ip'); + } + + return ''; + }) + ->afterStateUpdated(function (Set $set, ?string $state) { + $set('dns', null); + $set('ip', null); + + [$subdomain] = str($state)->explode('.', 2); + if (!is_numeric($subdomain)) { + $set('name', $subdomain); + } + + if (!$state || is_ip($state)) { + $set('dns', null); + + return; + } + + $ip = get_ip_from_hostname($state); + if ($ip) { + $set('dns', true); + + $set('ip', $ip); + } else { + $set('dns', false); + } + }) + ->maxLength(255), + TextInput::make('ip') + ->disabled() + ->hidden(), + ToggleButtons::make('dns') + ->label(trans('admin/node.dns')) + ->helperText(trans('admin/node.dns_help')) + ->disabled() + ->inline() + ->default(null) + ->hint(fn (Get $get) => $get('ip')) + ->hintColor('success') + ->stateCast(new BooleanStateCast(false, true)) + ->options([ + 1 => trans('admin/node.valid'), + 0 => trans('admin/node.invalid'), + ]) + ->colors([ + 1 => 'success', + 0 => 'danger', + ]) + ->columnSpan(1), + TextInput::make('daemon_connect') + ->columnSpan(1) + ->label(fn (Get $get) => $get('connection') === 'https_proxy' ? trans('admin/node.connect_port') : trans('admin/node.port')) + ->helperText(fn (Get $get) => $get('connection') === 'https_proxy' ? trans('admin/node.connect_port_help') : trans('admin/node.port_help')) + ->minValue(1) + ->maxValue(65535) + ->default(8080) + ->required() + ->integer(), + TextInput::make('name') + ->label(trans('admin/node.display_name')) + ->columnSpan([ + 'default' => 1, + 'sm' => 1, + 'md' => 1, + 'lg' => 2, + ]) + ->required() + ->maxLength(100), + Hidden::make('scheme'), + Hidden::make('behind_proxy'), + ToggleButtons::make('connection') + ->label(trans('admin/node.ssl')) + ->columnSpan(1) + ->inline() + ->helperText(function (Get $get) { + if (request()->isSecure()) { + return new HtmlString(trans('admin/node.panel_on_ssl')); + } + + if (is_ip($get('fqdn'))) { + return trans('admin/node.ssl_help'); + } + + return ''; + }) + ->disableOptionWhen(fn (string $value) => $value === 'http' && request()->isSecure()) + ->options([ + 'http' => 'HTTP', + 'https' => 'HTTPS (SSL)', + 'https_proxy' => 'HTTPS with (reverse) proxy', + ]) + ->colors([ + 'http' => 'warning', + 'https' => 'success', + 'https_proxy' => 'success', + ]) + ->icons([ + 'http' => 'tabler-lock-open-off', + 'https' => 'tabler-lock', + 'https_proxy' => 'tabler-shield-lock', + ]) + ->formatStateUsing(fn (Get $get) => $get('scheme') === 'http' ? 'http' : ($get('behind_proxy') ? 'https_proxy' : 'https')) + ->live() + ->dehydrated(false) + ->afterStateUpdated(function ($state, Set $set) { + $set('scheme', $state === 'http' ? 'http' : 'https'); + $set('behind_proxy', $state === 'https_proxy'); + + $set('daemon_connect', $state === 'https_proxy' ? 443 : 8080); + $set('daemon_listen', 8080); + }), + TextInput::make('daemon_listen') + ->columnSpan(1) + ->label(trans('admin/node.listen_port')) + ->helperText(trans('admin/node.listen_port_help')) + ->minValue(1) + ->maxValue(65535) + ->default(8080) + ->required() + ->integer() + ->visible(fn (Get $get) => $get('connection') === 'https_proxy'), + ]), + Tab::make('advanced_settings') + ->label(trans('admin/node.tabs.advanced_settings')) + ->columns([ + 'default' => 1, + 'sm' => 1, + 'md' => 4, + 'lg' => 6, + ]) + ->icon('tabler-server-cog') + ->schema([ + TextInput::make('id') + ->label(trans('admin/node.node_id')) + ->columnSpan([ + 'default' => 1, + 'sm' => 1, + 'md' => 2, + 'lg' => 1, + ]) + ->disabled(), + TextInput::make('uuid') + ->columnSpan([ + 'default' => 1, + 'sm' => 1, + 'md' => 2, + 'lg' => 2, + ]) + ->label(trans('admin/node.node_uuid')) + ->hintCopy() + ->disabled(), + TagsInput::make('tags') + ->label(trans('admin/node.tags')) + ->placeholder('') + ->columnSpan([ + 'default' => 1, + 'sm' => 1, + 'md' => 2, + 'lg' => 2, + ]), + TextInput::make('upload_size') + ->columnSpan([ + 'default' => 1, + 'sm' => 1, + 'md' => 2, + 'lg' => 1, + ]) + ->label(trans('admin/node.upload_limit')) + ->hintIcon('tabler-question-mark', trans('admin/node.upload_limit_help.0') . trans('admin/node.upload_limit_help.1')) + ->numeric()->required() + ->minValue(1) + ->maxValue(1024) + ->suffix(config('panel.use_binary_prefix') ? 'MiB' : 'MB'), + TextInput::make('daemon_sftp') + ->columnSpan([ + 'default' => 1, + 'sm' => 1, + 'md' => 1, + 'lg' => 3, + ]) + ->label(trans('admin/node.sftp_port')) + ->minValue(1) + ->maxValue(65535) + ->default(2022) + ->required() + ->integer(), + TextInput::make('daemon_sftp_alias') + ->columnSpan([ + 'default' => 1, + 'sm' => 1, + 'md' => 1, + 'lg' => 3, + ]) + ->label(trans('admin/node.sftp_alias')) + ->helperText(trans('admin/node.sftp_alias_help')), + ToggleButtons::make('public') + ->columnSpan([ + 'default' => 1, + 'sm' => 1, + 'md' => 1, + 'lg' => 3, + ]) + ->label(trans('admin/node.use_for_deploy')) + ->inline() + ->stateCast(new BooleanStateCast(false, true)) + ->options([ + 1 => trans('admin/node.yes'), + 0 => trans('admin/node.no'), + ]) + ->colors([ + 1 => 'success', + 0 => 'danger', + ]), + ToggleButtons::make('maintenance_mode') + ->columnSpan([ + 'default' => 1, + 'sm' => 1, + 'md' => 1, + 'lg' => 3, + ]) + ->label(trans('admin/node.maintenance_mode')) + ->inline() + ->hintIcon('tabler-question-mark', trans('admin/node.maintenance_mode_help')) + ->stateCast(new BooleanStateCast(false, true)) + ->options([ + 1 => trans('admin/node.enabled'), + 0 => trans('admin/node.disabled'), + ]) + ->colors([ + 1 => 'danger', + 0 => 'success', + ]), + // Subdomains plugin field + TextInput::make('srv_target') + ->label(trans('subdomains::strings.srv_target')) + ->placeholder('play.example.com') + ->helperText(trans('subdomains::strings.srv_target_help')) + ->rules(['nullable', 'string', 'regex:/^(?=.{1,253}$)(?!-)[A-Za-z0-9-]{1,63}(?columnSpan([ + 'default' => 1, + 'sm' => 1, + 'md' => 1, + 'lg' => 3, + ]) + ->dehydrateStateUsing(function ($state) { + $this->record->forceFill(['srv_target' => $state])->save(); + + Notification::make() + ->title(trans('subdomains::strings.notifications.srv_target_updated_title')) + ->body(trans('subdomains::strings.notifications.srv_target_updated')) + ->warning() + ->send(); + + return $state; + }), + Grid::make() + ->columns([ + 'default' => 1, + 'sm' => 1, + 'md' => 3, + 'lg' => 6, + ]) + ->columnSpanFull() + ->schema([ + ToggleButtons::make('unlimited_mem') + ->dehydrated() + ->label(trans('admin/node.memory'))->inlineLabel()->inline() + ->afterStateUpdated(fn (Set $set) => $set('memory', 0)) + ->afterStateUpdated(fn (Set $set) => $set('memory_overallocate', 0)) + ->formatStateUsing(fn (Get $get) => $get('memory') == 0) + ->live() + ->stateCast(new BooleanStateCast(false, true)) + ->options([ + 1 => trans('admin/node.unlimited'), + 0 => trans('admin/node.limited'), + ]) + ->colors([ + 1 => 'primary', + 0 => 'warning', + ]) + ->columnSpan([ + 'default' => 1, + 'sm' => 1, + 'md' => 1, + 'lg' => 2, + ]), + TextInput::make('memory') + ->dehydratedWhenHidden() + ->hidden(fn (Get $get) => $get('unlimited_mem')) + ->label(trans('admin/node.memory_limit'))->inlineLabel() + ->suffix(config('panel.use_binary_prefix') ? 'MiB' : 'MB') + ->required() + ->columnSpan([ + 'default' => 1, + 'sm' => 1, + 'md' => 1, + 'lg' => 2, + ]) + ->numeric() + ->minValue(0), + TextInput::make('memory_overallocate') + ->dehydratedWhenHidden() + ->label(trans('admin/node.overallocate'))->inlineLabel() + ->required() + ->hidden(fn (Get $get) => $get('unlimited_mem')) + ->columnSpan([ + 'default' => 1, + 'sm' => 1, + 'md' => 1, + 'lg' => 2, + ]) + ->numeric() + ->minValue(-1) + ->maxValue(100) + ->suffix('%'), + ]), + Grid::make() + ->columnSpanFull() + ->columns([ + 'default' => 1, + 'sm' => 1, + 'md' => 3, + 'lg' => 6, + ]) + ->schema([ + ToggleButtons::make('unlimited_disk') + ->dehydrated() + ->label(trans('admin/node.disk'))->inlineLabel()->inline() + ->live() + ->afterStateUpdated(fn (Set $set) => $set('disk', 0)) + ->afterStateUpdated(fn (Set $set) => $set('disk_overallocate', 0)) + ->formatStateUsing(fn (Get $get) => $get('disk') == 0) + ->stateCast(new BooleanStateCast(false, true)) + ->options([ + 1 => trans('admin/node.unlimited'), + 0 => trans('admin/node.limited'), + ]) + ->colors([ + 1 => 'primary', + 0 => 'warning', + ]) + ->columnSpan([ + 'default' => 1, + 'sm' => 1, + 'md' => 1, + 'lg' => 2, + ]), + TextInput::make('disk') + ->dehydratedWhenHidden() + ->hidden(fn (Get $get) => $get('unlimited_disk')) + ->label(trans('admin/node.disk_limit'))->inlineLabel() + ->suffix(config('panel.use_binary_prefix') ? 'MiB' : 'MB') + ->required() + ->columnSpan([ + 'default' => 1, + 'sm' => 1, + 'md' => 1, + 'lg' => 2, + ]) + ->numeric() + ->minValue(0), + TextInput::make('disk_overallocate') + ->dehydratedWhenHidden() + ->hidden(fn (Get $get) => $get('unlimited_disk')) + ->label(trans('admin/node.overallocate'))->inlineLabel() + ->columnSpan([ + 'default' => 1, + 'sm' => 1, + 'md' => 1, + 'lg' => 2, + ]) + ->required() + ->numeric() + ->minValue(-1) + ->maxValue(100) + ->suffix('%'), + ]), + Grid::make() + ->columns(6) + ->columnSpanFull() + ->schema([ + ToggleButtons::make('unlimited_cpu') + ->dehydrated() + ->label(trans('admin/node.cpu'))->inlineLabel()->inline() + ->live() + ->afterStateUpdated(fn (Set $set) => $set('cpu', 0)) + ->afterStateUpdated(fn (Set $set) => $set('cpu_overallocate', 0)) + ->formatStateUsing(fn (Get $get) => $get('cpu') == 0) + ->stateCast(new BooleanStateCast(false, true)) + ->options([ + 1 => trans('admin/node.unlimited'), + 0 => trans('admin/node.limited'), + ]) + ->colors([ + 1 => 'primary', + 0 => 'warning', + ]) + ->columnSpan(2), + TextInput::make('cpu') + ->dehydratedWhenHidden() + ->hidden(fn (Get $get) => $get('unlimited_cpu')) + ->label(trans('admin/node.cpu_limit'))->inlineLabel() + ->suffix('%') + ->required() + ->columnSpan(2) + ->numeric() + ->minValue(0), + TextInput::make('cpu_overallocate') + ->dehydratedWhenHidden() + ->hidden(fn (Get $get) => $get('unlimited_cpu')) + ->label(trans('admin/node.overallocate'))->inlineLabel() + ->columnSpan(2) + ->required() + ->numeric() + ->minValue(-1) + ->maxValue(100) + ->suffix('%'), + ]), + ]), + Tab::make('config_file') + ->label(trans('admin/node.tabs.config_file')) + ->icon('tabler-code') + ->schema([ + TextEntry::make('instructions') + ->label(trans('admin/node.instructions')) + ->columnSpanFull() + ->state(new HtmlString(trans('admin/node.instructions_help'))), + CodeEntry::make('config') + ->label('/etc/pelican/config.yml') + ->grammar(Grammar::Yaml) + ->state(fn (Node $node) => $node->getYamlConfiguration()) + ->copyable() + ->disabled() + ->columnSpanFull(), + Grid::make() + ->columns() + ->columnSpanFull() + ->schema([ + Actions::make([ + Action::make('autoDeploy') + ->label(trans('admin/node.auto_deploy')) + ->color('primary') + ->modalHeading(trans('admin/node.auto_deploy')) + ->icon('tabler-rocket') + ->modalSubmitAction(false) + ->modalCancelAction(false) + ->modalFooterActionsAlignment(Alignment::Center) + ->schema([ + ToggleButtons::make('docker') + ->label(trans('admin/node.auto_label')) + ->live() + ->helperText(trans('admin/node.auto_question')) + ->inline() + ->default(false) + ->afterStateUpdated(fn (bool $state, NodeAutoDeployService $service, Node $node, Set $set) => $set('generatedToken', $service->handle(request(), $node, $state))) + ->stateCast(new BooleanStateCast(false, true)) + ->options([ + 0 => trans('admin/node.standalone'), + 1 => trans('admin/node.docker'), + ]) + ->colors([ + 0 => 'primary', + 1 => 'success', + ]) + ->columnSpan(1), + Textarea::make('generatedToken') + ->label(trans('admin/node.auto_command')) + ->readOnly() + ->autosize() + ->hintCopy() + ->formatStateUsing(fn (NodeAutoDeployService $service, Node $node, Set $set, Get $get) => $set('generatedToken', $service->handle(request(), $node, $get('docker')))), + ]) + ->mountUsing(function (Schema $schema) { + $schema->fill(); + }), + ])->fullWidth(), + Actions::make([ + Action::make('resetKey') + ->label(trans('admin/node.reset_token')) + ->color('danger') + ->requiresConfirmation() + ->modalHeading(trans('admin/node.reset_token')) + ->modalDescription(trans('admin/node.reset_help')) + ->action(function (Node $node) { + try { + $this->nodeUpdateService->handle($node, [], true); + } catch (Exception) { + Notification::make() + ->title(trans('admin/node.error_connecting', ['node' => $node->name])) + ->body(trans('admin/node.error_connecting_description')) + ->color('warning') + ->icon('tabler-database') + ->warning() + ->send(); + + } + Notification::make()->success()->title(trans('admin/node.token_reset'))->send(); + $this->fillForm(); + }), + ])->fullWidth(), + ]), + ]), + Tab::make('diagnostics') + ->label(trans('admin/node.tabs.diagnostics')) + ->icon('tabler-heart-search') + ->schema([ + Section::make('diag') + ->heading(trans('admin/node.tabs.diagnostics')) + ->columnSpanFull() + ->columns(4) + ->disabled(fn (Get $get) => $get('pulled')) + ->headerActions([ + Action::make('pull') + ->label(trans('admin/node.diagnostics.pull')) + ->icon('tabler-cloud-download')->iconButton()->iconSize(IconSize::ExtraLarge) + ->hidden(fn (Get $get) => $get('pulled')) + ->action(function (Get $get, Set $set, Node $node) { + $includeEndpoints = $get('include_endpoints') ?? true; + $includeLogs = $get('include_logs') ?? true; + $logLines = $get('log_lines') ?? 200; + + try { + $response = $this->daemonSystemRepository->setNode($node)->getDiagnostics($logLines, $includeEndpoints, $includeLogs); + + if ($response->status() === 404) { + Notification::make() + ->title(trans('admin/node.diagnostics.404')) + ->warning() + ->send(); + + return; + } + + $set('pulled', true); + $set('uploaded', false); + $set('log', $response->body()); + + Notification::make() + ->title(trans('admin/node.diagnostics.logs_pulled')) + ->success() + ->send(); + } catch (ConnectionException $e) { + Notification::make() + ->title(trans('admin/node.error_connecting', ['node' => $node->name])) + ->body($e->getMessage()) + ->danger() + ->send(); + + } + }), + Action::make('upload') + ->label(trans('admin/node.diagnostics.upload')) + ->visible(fn (Get $get) => $get('pulled') ?? false) + ->icon('tabler-cloud-upload')->iconButton()->iconSize(IconSize::ExtraLarge) + ->action(function (Get $get, Set $set) { + try { + $response = Http::asMultipart() + ->attach('c', $get('log')) + ->attach('e', '14d') + ->post('https://logs.pelican.dev'); + + if ($response->failed()) { + Notification::make() + ->title(trans('admin/node.diagnostics.upload_failed')) + ->body(fn () => $response->status() . ' - ' . $response->body()) + ->danger() + ->send(); + + return; + } + + $data = $response->json(); + $url = $data['url']; + + Notification::make() + ->title(trans('admin/node.diagnostics.logs_uploaded')) + ->body("{$url}") + ->success() + ->actions([ + Action::make('viewLogs') + ->label(trans('admin/node.diagnostics.view_logs')) + ->url($url) + ->openUrlInNewTab(true), + ]) + ->persistent() + ->send(); + $set('log', $url); + $set('pulled', false); + $set('uploaded', true); + + } catch (\Exception $e) { + Notification::make() + ->title(trans('admin/node.diagnostics.upload_failed')) + ->body($e->getMessage()) + ->danger() + ->send(); + } + }), + Action::make('clear') + ->label(trans('admin/node.diagnostics.clear')) + ->visible(fn (Get $get) => $get('pulled') ?? false) + ->icon('tabler-trash')->iconButton()->iconSize(IconSize::ExtraLarge)->color('danger') + ->action(function (Get $get, Set $set) { + $set('pulled', false); + $set('uploaded', false); + $set('log', null); + $this->refresh(); + } + ), + ]) + ->schema([ + ToggleButtons::make('include_endpoints') + ->hintIcon('tabler-question-mark')->inline() + ->hintIconTooltip(trans('admin/node.diagnostics.include_endpoints_hint')) + ->formatStateUsing(fn () => 1) + ->boolean(), + ToggleButtons::make('include_logs') + ->live() + ->hintIcon('tabler-question-mark')->inline() + ->hintIconTooltip(trans('admin/node.diagnostics.include_logs_hint')) + ->formatStateUsing(fn () => 1) + ->boolean(), + Slider::make('log_lines') + ->columnSpan(2) + ->hiddenLabel() + ->live() + ->tooltips(RawJs::make(<<<'JS' + `${$value} lines` + JS)) + ->visible(fn (Get $get) => $get('include_logs')) + ->range(minValue: 100, maxValue: 500) + ->pips(PipsMode::Steps, density: 10) + ->step(50) + ->formatStateUsing(fn () => 200) + ->fillTrack(), + Hidden::make('pulled'), + Hidden::make('uploaded'), + ]), + Textarea::make('log') + ->hiddenLabel() + ->columnSpanFull() + ->rows(35) + ->visible(fn (Get $get) => ($get('pulled') ?? false) || ($get('uploaded') ?? false)), + ]), + ]), + ]); + } +} \ No newline at end of file diff --git a/subdomains/src/Filament/Admin/Resources/CloudflareDomains/CloudflareDomainResource.php b/subdomains/src/Filament/Admin/Resources/CloudflareDomains/CloudflareDomainResource.php index 7108768..571166f 100644 --- a/subdomains/src/Filament/Admin/Resources/CloudflareDomains/CloudflareDomainResource.php +++ b/subdomains/src/Filament/Admin/Resources/CloudflareDomains/CloudflareDomainResource.php @@ -53,8 +53,6 @@ public static function table(Table $table): Table ->columns([ TextColumn::make('name') ->label(trans('subdomains::strings.name')), - TextColumn::make('srv_target') - ->label(trans('subdomains::strings.srv_target')), TextColumn::make('subdomains_count') ->label(trans_choice('subdomains::strings.subdomain', 2)) ->counts('subdomains'), @@ -79,11 +77,6 @@ public static function form(Schema $schema): Schema ->label(trans('subdomains::strings.name')) ->required() ->unique(), - TextInput::make('srv_target') - ->label(trans('subdomains::strings.srv_target')) - ->helperText(trans('subdomains::strings.srv_target_help')) - ->placeholder('play.example.com') - ->rules(['nullable', 'string', 'regex:/^(?=.{1,253}$)(?!-)[A-Za-z0-9-]{1,63}(?label(trans('subdomains::strings.name')) ->state(fn (Subdomain $subdomain) => $subdomain->getLabel()), - ToggleColumn::make('srv_record') - ->label(trans('subdomains::strings.srv_record')) - ->tooltip(fn (Subdomain $record) => $record->domain && $record->domain->srv_target ? trans('subdomains::strings.srv_record_help') : trans('subdomains::strings.srv_target_missing')) - ->disabled(fn (Subdomain $record) => !$record->domain || empty($record->domain->srv_target)), + TextColumn::make('record_type') + ->label(trans('subdomains::strings.record_type')) + ->icon(fn (Subdomain $subdomain) => $subdomain->srv_record && empty($subdomain->server?->node?->srv_target) ? 'tabler-alert-triangle' : null) + ->color(fn (Subdomain $subdomain) => $subdomain->srv_record && empty($subdomain->server?->node?->srv_target) ? 'danger' : null) + ->helperText(fn (Subdomain $subdomain) => $subdomain->srv_record && empty($subdomain->server?->node?->srv_target) ? trans('subdomains::strings.srv_target_missing') : null), ]) ->recordActions([ EditAction::make() @@ -122,10 +123,9 @@ public static function form(Schema $schema): Schema ->searchable(), Toggle::make('srv_record') ->label(trans('subdomains::strings.srv_record')) - ->helperText(trans('subdomains::strings.srv_record_help')) + ->helperText(fn () => Filament::getTenant()->node->srv_target ? trans('subdomains::strings.srv_record_help') : trans('subdomains::strings.srv_target_missing')) ->reactive() - ->disabled(fn (callable $get) => (empty($get('domain_id')) || empty(CloudflareDomain::find($get('domain_id'))?->srv_target))) - ->default(false), + ->disabled(fn () => empty(Filament::getTenant()->node->srv_target)), ]); } diff --git a/subdomains/src/Models/CloudflareDomain.php b/subdomains/src/Models/CloudflareDomain.php index 1dbd87c..d98bad9 100644 --- a/subdomains/src/Models/CloudflareDomain.php +++ b/subdomains/src/Models/CloudflareDomain.php @@ -18,7 +18,6 @@ class CloudflareDomain extends Model protected $fillable = [ 'name', 'cloudflare_id', - 'srv_target', ]; protected static function boot(): void diff --git a/subdomains/src/Providers/SubdomainsPluginProvider.php b/subdomains/src/Providers/SubdomainsPluginProvider.php index 6ce1afa..4bd2198 100644 --- a/subdomains/src/Providers/SubdomainsPluginProvider.php +++ b/subdomains/src/Providers/SubdomainsPluginProvider.php @@ -2,10 +2,11 @@ namespace Boy132\Subdomains\Providers; +use App\Filament\Admin\Resources\Nodes\NodeResource; use App\Filament\Admin\Resources\Servers\ServerResource; use App\Models\Role; use App\Models\Server; -use Boy132\Subdomains\Filament\Admin\Resources\Users\RelationManagers\SubdomainRelationManager; +use Boy132\Subdomains\Filament\Admin\Resources\Nodes\Pages\EditNode; use Boy132\Subdomains\Models\Subdomain; use Illuminate\Support\Facades\Http; use Illuminate\Support\ServiceProvider; @@ -14,6 +15,7 @@ class SubdomainsPluginProvider extends ServiceProvider { public function register(): void { + NodeResource::registerCustomPages(['edit' => EditNode::route('/{record}/edit')]); ServerResource::registerCustomRelations(SubdomainRelationManager::class); Role::registerCustomDefaultPermissions('cloudflare_domain'); From c476a60cb3cbca9474759ba8ed928164454ccc8a Mon Sep 17 00:00:00 2001 From: HarlequinSin Date: Thu, 1 Jan 2026 20:02:53 +1100 Subject: [PATCH 35/51] lang updates for new notifications --- subdomains/lang/en/strings.php | 28 ++++++++++++++++++---------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/subdomains/lang/en/strings.php b/subdomains/lang/en/strings.php index 3b8f387..0d42305 100644 --- a/subdomains/lang/en/strings.php +++ b/subdomains/lang/en/strings.php @@ -3,39 +3,44 @@ return [ 'no_domains' => 'No Domains', 'domain' => 'Domain|Domains', - 'no_subdomains' => 'No Subdomains', 'subdomain' => 'Subdomain|Subdomains', + 'name' => 'Name', + 'limit' => 'Limit', 'change_limit' => 'Change Limit', 'limit_changed' => 'Limit changed', 'limit_reached' => 'Subdomain Limit Reached', - 'create_subdomain' => 'Create Subdomain', - 'name' => 'Name', + 'create_subdomain' => 'Create Subdomain', + 'subdomain_change_limit' => "Change Subdomain Limit", + 'subdomain_limit' => "Subdomain Limit", + 'record_type' => 'Record Type', 'srv_record' => 'SRV Record', 'srv_record_help' => 'Enable this option to create a SRV record instead of an A or AAAA record.', + 'set_srv_target' => 'Set SRV Target', 'srv_target' => 'SRV Target', 'srv_target_help' => 'The hostname that SRV records point to (for example: play.example.com).', - - 'errors' => [ - 'srv_target_missing' => 'Cannot enable SRV record because the selected domain does not have an SRV target set.', - ], + 'srv_target_missing' => 'SRV record is missing from node configuration.', + 'srv_target_confirmation' => 'Changing the SRV target will require all existing subdomains using SRV records to be updated. Are you sure you want to continue?', 'api_token' => 'Cloudflare API Token', 'api_token_help' => 'The token needs to have read permissions for Zone.Zone and write for Zone.Dns. For better security you can also set the "Zone Resources" to exclude certain domains and add the panel ip to the "Client IP Adress Filtering".', 'notifications' => [ + 'srv_target_updated_title' => 'SRV target changed successfully.', + 'srv_target_updated' => 'Existing subdomains using SRV records will need to be updated to use the new target.', + 'cloudflare_missing_zone_title' => 'Cloudflare: Missing Zone ID', 'cloudflare_missing_zone' => 'Cloudflare zone ID is not configured for :domain. Cannot save DNS record for :subdomain.', 'cloudflare_missing_srv_port_title' => 'Cloudflare: Missing SRV Port', - 'cloudflare_missing_srv_port' => 'SRV port is missing for :subdomain. Cannot save SRV record.', + 'cloudflare_missing_srv_port' => 'SRV port is missing for :server.', 'cloudflare_missing_srv_target_title' => 'Cloudflare: Missing SRV Target', - 'cloudflare_missing_srv_target' => 'SRV target is missing for :subdomain. Cannot save SRV record.', + 'cloudflare_missing_srv_target' => 'SRV target is missing from :node. ', 'cloudflare_record_updated_title' => 'Cloudflare: Record Updated', 'cloudflare_record_updated' => 'Successfully updated :subdomain record to :record_type', @@ -55,6 +60,9 @@ 'cloudflare_zone_fetch_failed' => 'Failed to fetch Cloudflare Zone ID for domain: :domain', 'cloudflare_domain_saved' => 'Successfully saved domain: :domain', - 'settings_saved' => 'Settings saved', + 'cloudflare_invalid_service_record_type_title' => 'Cloudflare: Unable to determine service type', + 'cloudflare_invalid_service_record_type' => 'Unable to determine service type for SRV record of :subdomain. Please check egg is correctly tagged', ], + + 'settings_saved' => 'Settings saved', ]; From 51252636a54376b51416e14d3a0b6cbf7ca2b232 Mon Sep 17 00:00:00 2001 From: HarlequinSin Date: Thu, 1 Jan 2026 20:07:10 +1100 Subject: [PATCH 36/51] Changing SRV record service from hard-coded for minecraft to based on egg tags (Only minecraft at this time) --- subdomains/src/Enums/ServiceRecordType.php | 58 +++++++++++++++++++ subdomains/src/Models/Subdomain.php | 32 ++++++---- subdomains/src/Services/CloudflareService.php | 2 +- 3 files changed, 81 insertions(+), 11 deletions(-) create mode 100644 subdomains/src/Enums/ServiceRecordType.php diff --git a/subdomains/src/Enums/ServiceRecordType.php b/subdomains/src/Enums/ServiceRecordType.php new file mode 100644 index 0000000..f3a2130 --- /dev/null +++ b/subdomains/src/Enums/ServiceRecordType.php @@ -0,0 +1,58 @@ +name)->title(); + } + + public static function isSupported(Server $server): bool + { + $tags = $server->egg->tags ?? []; + foreach (self::cases() as $type) { + if (in_array($type->name, $tags)) { + return true; + } + } + return false; + } + + public static function fromServer(Server $server): ?self + { + $tags = $server->egg->tags ?? []; + return self::fromTags($tags); + } + + /** @param string[] $tags */ + public static function fromTags(array $tags): ?self + { + foreach (self::cases() as $type) { + if (in_array($type->name, $tags)) { + return $type; + } + } + + return null; + } + + public function service(): string + { + $parts = explode('.', $this->value); + return $parts[0]; + } + + public function protocol(): string + { + $parts = explode('.', $this->value); + return $parts[1]; + } +} diff --git a/subdomains/src/Models/Subdomain.php b/subdomains/src/Models/Subdomain.php index 7288429..96b0282 100644 --- a/subdomains/src/Models/Subdomain.php +++ b/subdomains/src/Models/Subdomain.php @@ -91,7 +91,7 @@ public function setRecordType($value): void protected function upsertOnCloudflare(): void { - $service = app(CloudflareService::class); + $registrar = app(CloudflareService::class); $zoneId = $this->domain->cloudflare_id; if (empty($zoneId)) { @@ -106,13 +106,12 @@ protected function upsertOnCloudflare(): void return; } - // SRV: target comes from domain, port from server allocation + // SRV: target comes from node, port from server allocation if ($this->record_type === 'SRV') { $port = $this->server && $this->server->allocation ? ($this->server->allocation->port ?? null) : null; if (empty($port)) { Log::warning('Server missing allocation with port', $this->toArray()); - Notification::make() ->danger() ->title(trans('subdomains::strings.notifications.cloudflare_missing_srv_port_title')) @@ -122,19 +121,32 @@ protected function upsertOnCloudflare(): void return; } - if (empty($this->domain->srv_target)) { - Log::warning('Domain missing SRV target for SRV record', ['domain_id' => $this->domain_id]); + $serviceRecordType = ServiceRecordType::fromServer($this->server); + if (!$serviceRecordType) { + Log::warning('Unable to determine service record type for SRV record', ['server_id' => $this->server->id, 'server' => $this->server->name]); + Notification::make() + ->danger() + ->title(trans('subdomains::strings.notifications.cloudflare_invalid_service_record_type_title')) + ->body(trans('subdomains::strings.notifications.cloudflare_invalid_service_record_type', ['subdomain' => $this->name . '.' . ($this->domain->name ?? 'unknown')])) + ->send(); + + return; + } + if (!$this->server || !$this->server->node || empty($this->server->node->srv_target)) { + Log::warning('Node missing SRV target for SRV record', ['node_id' => $this->server->node?->id ?? null]); Notification::make() ->danger() ->title(trans('subdomains::strings.notifications.cloudflare_missing_srv_target_title')) - ->body(trans('subdomains::strings.notifications.cloudflare_missing_srv_target', ['subdomain' => $this->name . '.' . ($this->domain->name ?? 'unknown')])) + ->body(trans('subdomains::strings.notifications.cloudflare_missing_srv_target', ['node' => $this->server->node->name])) ->send(); return; } - $result = $service->upsertDnsRecord($zoneId, $this->name, 'SRV', $this->domain->srv_target, $this->cloudflare_id, $port); + $recordName = sprintf('%s.%s.%s', $serviceRecordType->service(), $serviceRecordType->protocol(), $this->name); + + $result = $registrar->upsertDnsRecord($zoneId, $recordName, 'SRV', $this->server->node->srv_target, $this->cloudflare_id, $port, $serviceRecordType); if ($result['success'] && !empty($result['id'])) { if ($this->cloudflare_id !== $result['id']) { @@ -172,7 +184,7 @@ protected function upsertOnCloudflare(): void return; } - $result = $service->upsertDnsRecord($zoneId, $this->name, $this->record_type, $this->server->allocation->ip, $this->cloudflare_id, null); + $result = $registrar->upsertDnsRecord($zoneId, $this->name, $this->record_type, $this->server->allocation->ip, $this->cloudflare_id, null); if ($result['success'] && !empty($result['id'])) { if ($this->cloudflare_id !== $result['id']) { @@ -201,9 +213,9 @@ protected function upsertOnCloudflare(): void protected function deleteOnCloudflare(): void { if ($this->cloudflare_id && $this->domain && $this->domain->cloudflare_id) { - $service = app(CloudflareService::class); + $registrar = app(CloudflareService::class); - $result = $service->deleteDnsRecord($this->domain->cloudflare_id, $this->cloudflare_id); + $result = $registrar->deleteDnsRecord($this->domain->cloudflare_id, $this->cloudflare_id); if (!empty($result['success'])) { Notification::make() diff --git a/subdomains/src/Services/CloudflareService.php b/subdomains/src/Services/CloudflareService.php index b0525e5..ab19184 100644 --- a/subdomains/src/Services/CloudflareService.php +++ b/subdomains/src/Services/CloudflareService.php @@ -64,7 +64,7 @@ public function upsertDnsRecord(string $zoneId, string $name, string $recordType } $payload = [ - 'name' => sprintf('_minecraft._tcp.%s', $name), + 'name' => $name, 'ttl' => $ttl, 'type' => 'SRV', 'comment' => $comment, From de62b89ce85bf8812c2c3393dcfff48954052749 Mon Sep 17 00:00:00 2001 From: HarlequinSin Date: Thu, 1 Jan 2026 20:26:22 +1100 Subject: [PATCH 37/51] cleaning up record_type/srv_record code --- subdomains/src/Models/Subdomain.php | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/subdomains/src/Models/Subdomain.php b/subdomains/src/Models/Subdomain.php index 96b0282..eaf8bca 100644 --- a/subdomains/src/Models/Subdomain.php +++ b/subdomains/src/Models/Subdomain.php @@ -36,17 +36,14 @@ class Subdomain extends Model implements HasLabel 'srv_record' => 'boolean', ]; + protected $appends = [ + 'srv_record', + ]; + protected static function boot(): void { parent::boot(); - static::saving(function (self $model) { - if (array_key_exists('srv_record', $model->attributes)) { - $model->setRecordType($model->attributes['srv_record']); - unset($model->attributes['srv_record']); - } - }); - static::saved(function (self $model) { $model->upsertOnCloudflare(); }); @@ -76,12 +73,13 @@ public function getSrvRecordAttribute(): bool return $this->record_type === 'SRV'; } - public function setRecordType($value): void + public function setSrvRecordAttribute($isSrvRecord): void { - if ($value) { + if ($isSrvRecord) { $this->attributes['record_type'] = 'SRV'; } else { - if ($this->server && $this->server->allocation && is_ipv6($this->server->allocation->ip)) { + $ip = $this->server?->allocation?->ip ?? null; + if (!empty($ip) && filter_var($ip, FILTER_FLAG_IPV6)) { $this->attributes['record_type'] = 'AAAA'; } else { $this->attributes['record_type'] = 'A'; From 8b7df3e32aa3ff45b4e3c1d92c8d78b90a372e30 Mon Sep 17 00:00:00 2001 From: HarlequinSin Date: Thu, 1 Jan 2026 20:27:27 +1100 Subject: [PATCH 38/51] Fixed undefined class --- subdomains/src/Models/Subdomain.php | 1 + 1 file changed, 1 insertion(+) diff --git a/subdomains/src/Models/Subdomain.php b/subdomains/src/Models/Subdomain.php index eaf8bca..e3560a9 100644 --- a/subdomains/src/Models/Subdomain.php +++ b/subdomains/src/Models/Subdomain.php @@ -3,6 +3,7 @@ namespace Boy132\Subdomains\Models; use App\Models\Server; +use Boy132\Subdomains\Enums\ServiceRecordType; use Boy132\Subdomains\Services\CloudflareService; use Filament\Notifications\Notification; use Filament\Support\Contracts\HasLabel; From 194243ca973dfe33241d265a80b1831984037681 Mon Sep 17 00:00:00 2001 From: HarlequinSin Date: Thu, 1 Jan 2026 20:29:14 +1100 Subject: [PATCH 39/51] cx and pint formatting --- subdomains/lang/en/strings.php | 4 ++-- subdomains/src/Enums/ServiceRecordType.php | 4 ++++ .../Server/Resources/Subdomains/SubdomainResource.php | 2 +- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/subdomains/lang/en/strings.php b/subdomains/lang/en/strings.php index 0d42305..6ad3ff7 100644 --- a/subdomains/lang/en/strings.php +++ b/subdomains/lang/en/strings.php @@ -13,8 +13,8 @@ 'limit_reached' => 'Subdomain Limit Reached', 'create_subdomain' => 'Create Subdomain', - 'subdomain_change_limit' => "Change Subdomain Limit", - 'subdomain_limit' => "Subdomain Limit", + 'subdomain_change_limit' => 'Change Subdomain Limit', + 'subdomain_limit' => 'Subdomain Limit', 'record_type' => 'Record Type', 'srv_record' => 'SRV Record', diff --git a/subdomains/src/Enums/ServiceRecordType.php b/subdomains/src/Enums/ServiceRecordType.php index f3a2130..05fa224 100644 --- a/subdomains/src/Enums/ServiceRecordType.php +++ b/subdomains/src/Enums/ServiceRecordType.php @@ -23,12 +23,14 @@ public static function isSupported(Server $server): bool return true; } } + return false; } public static function fromServer(Server $server): ?self { $tags = $server->egg->tags ?? []; + return self::fromTags($tags); } @@ -47,12 +49,14 @@ public static function fromTags(array $tags): ?self public function service(): string { $parts = explode('.', $this->value); + return $parts[0]; } public function protocol(): string { $parts = explode('.', $this->value); + return $parts[1]; } } diff --git a/subdomains/src/Filament/Server/Resources/Subdomains/SubdomainResource.php b/subdomains/src/Filament/Server/Resources/Subdomains/SubdomainResource.php index e017dc6..9d2e40f 100644 --- a/subdomains/src/Filament/Server/Resources/Subdomains/SubdomainResource.php +++ b/subdomains/src/Filament/Server/Resources/Subdomains/SubdomainResource.php @@ -19,7 +19,6 @@ use Filament\Schemas\Schema; use Filament\Support\Enums\IconSize; use Filament\Tables\Columns\TextColumn; -use Filament\Tables\Columns\ToggleColumn; use Filament\Tables\Table; class SubdomainResource extends Resource @@ -112,6 +111,7 @@ public static function form(Schema $schema): Schema TextInput::make('name') ->label(trans('subdomains::strings.name')) ->required() + ->suffix(fn (callable $get) => CloudflareDomain::find($get('domain_id'))?->name) ->unique(), Select::make('domain_id') ->label(trans_choice('subdomains::strings.domain', 1)) From 079563f613b231714fbaba0441bd73547b00ad6e Mon Sep 17 00:00:00 2001 From: HarlequinSin Date: Thu, 1 Jan 2026 20:44:39 +1100 Subject: [PATCH 40/51] pint style fixes --- .../005_add_srv_target_to_nodes.php | 60 +++++++++---------- .../Filament/Admin/Nodes/Pages/EditNode.php | 22 ++++--- 2 files changed, 40 insertions(+), 42 deletions(-) diff --git a/subdomains/database/migrations/005_add_srv_target_to_nodes.php b/subdomains/database/migrations/005_add_srv_target_to_nodes.php index cab88da..dc30a4b 100644 --- a/subdomains/database/migrations/005_add_srv_target_to_nodes.php +++ b/subdomains/database/migrations/005_add_srv_target_to_nodes.php @@ -1,30 +1,30 @@ -dropColumn('srv_target'); - }); - - Schema::table('nodes', function (Blueprint $table) { - $table->string('srv_target')->nullable()->after('fqdn'); - }); - } - - public function down(): void - { - Schema::table('cloudflare_domains', function (Blueprint $table) { - $table->string('srv_target')->nullable()->after('cloudflare_id'); - }); - - Schema::table('nodes', function (Blueprint $table) { - $table->dropColumn('srv_target'); - }); - } -}; +dropColumn('srv_target'); + }); + + Schema::table('nodes', function (Blueprint $table) { + $table->string('srv_target')->nullable()->after('fqdn'); + }); + } + + public function down(): void + { + Schema::table('cloudflare_domains', function (Blueprint $table) { + $table->string('srv_target')->nullable()->after('cloudflare_id'); + }); + + Schema::table('nodes', function (Blueprint $table) { + $table->dropColumn('srv_target'); + }); + } +}; diff --git a/subdomains/src/Filament/Admin/Nodes/Pages/EditNode.php b/subdomains/src/Filament/Admin/Nodes/Pages/EditNode.php index c819888..cc5cd7c 100644 --- a/subdomains/src/Filament/Admin/Nodes/Pages/EditNode.php +++ b/subdomains/src/Filament/Admin/Nodes/Pages/EditNode.php @@ -2,19 +2,12 @@ namespace Boy132\Subdomains\Filament\Admin\Resources\Nodes\Pages; -use App\Filament\Admin\Resources\Nodes\NodeResource; use App\Filament\Admin\Resources\Nodes\Pages\EditNode as BaseEditNode; use App\Models\Node; -use App\Repositories\Daemon\DaemonSystemRepository; use App\Services\Helpers\SoftwareVersionService; use App\Services\Nodes\NodeAutoDeployService; -use App\Services\Nodes\NodeUpdateService; -use App\Traits\Filament\CanCustomizeHeaderActions; -use App\Traits\Filament\CanCustomizeHeaderWidgets; -use Boy132\Subdomains\Models\Subdomain; use Exception; use Filament\Actions\Action; -use Filament\Actions\DeleteAction; use Filament\Forms\Components\Hidden; use Filament\Forms\Components\Slider; use Filament\Forms\Components\Slider\Enums\PipsMode; @@ -25,7 +18,6 @@ use Filament\Infolists\Components\CodeEntry; use Filament\Infolists\Components\TextEntry; use Filament\Notifications\Notification; -use Filament\Resources\Pages\EditRecord; use Filament\Schemas\Components\Actions; use Filament\Schemas\Components\Fieldset; use Filament\Schemas\Components\Grid; @@ -42,11 +34,9 @@ use Filament\Support\RawJs; use Illuminate\Http\Client\ConnectionException; use Illuminate\Support\Facades\Http; -use Illuminate\Support\HtmlString; use Illuminate\Support\Facades\Log; +use Illuminate\Support\HtmlString; use Phiki\Grammar\Grammar; -use Boy132\Subdomains\Jobs\UpdateServerSubdomains; -use App\Models\Server; class EditNode extends BaseEditNode { @@ -395,6 +385,14 @@ public function form(Schema $schema): Schema ->warning() ->send(); + // Update all subdomains on servers linked to this node + $serverIds = Server::where('node_id', $this->record->id)->pluck('id'); + Log::info('Updating subdomains for Node ID ', ['node_id' => $this->record->id, 'serverIds' => $serverIds]); + + foreach ($serverIds as $serverId) { + UpdateServerSubdomains::dispatch($serverId, $state); + } + return $state; }), Grid::make() @@ -789,4 +787,4 @@ public function form(Schema $schema): Schema ]), ]); } -} \ No newline at end of file +} From 12db61c90ce6eff7c48dd3c5bd3d951970c03c02 Mon Sep 17 00:00:00 2001 From: HarlequinSin Date: Thu, 1 Jan 2026 21:00:54 +1100 Subject: [PATCH 41/51] moving to different branch --- subdomains/src/Filament/Admin/Nodes/Pages/EditNode.php | 8 -------- 1 file changed, 8 deletions(-) diff --git a/subdomains/src/Filament/Admin/Nodes/Pages/EditNode.php b/subdomains/src/Filament/Admin/Nodes/Pages/EditNode.php index cc5cd7c..cfe48d4 100644 --- a/subdomains/src/Filament/Admin/Nodes/Pages/EditNode.php +++ b/subdomains/src/Filament/Admin/Nodes/Pages/EditNode.php @@ -385,14 +385,6 @@ public function form(Schema $schema): Schema ->warning() ->send(); - // Update all subdomains on servers linked to this node - $serverIds = Server::where('node_id', $this->record->id)->pluck('id'); - Log::info('Updating subdomains for Node ID ', ['node_id' => $this->record->id, 'serverIds' => $serverIds]); - - foreach ($serverIds as $serverId) { - UpdateServerSubdomains::dispatch($serverId, $state); - } - return $state; }), Grid::make() From f2ba928534551b687b4805d7efb260be437c8e50 Mon Sep 17 00:00:00 2001 From: HarlequinSin Date: Thu, 1 Jan 2026 21:01:09 +1100 Subject: [PATCH 42/51] shortened/simplified all npe checks --- subdomains/src/Models/Subdomain.php | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/subdomains/src/Models/Subdomain.php b/subdomains/src/Models/Subdomain.php index e3560a9..5da74ea 100644 --- a/subdomains/src/Models/Subdomain.php +++ b/subdomains/src/Models/Subdomain.php @@ -92,14 +92,15 @@ protected function upsertOnCloudflare(): void { $registrar = app(CloudflareService::class); - $zoneId = $this->domain->cloudflare_id; + // add null check for domain + $zoneId = $this->domain?->cloudflare_id ?? null; if (empty($zoneId)) { Log::warning('Cloudflare zone id missing for domain', ['domain_id' => $this->domain_id]); Notification::make() ->danger() ->title(trans('subdomains::strings.notifications.cloudflare_missing_zone_title')) - ->body(trans('subdomains::strings.notifications.cloudflare_missing_zone', ['domain' => $this->domain->name ?? 'unknown', 'subdomain' => $this->name . '.' . ($this->domain->name ?? 'unknown')])) + ->body(trans('subdomains::strings.notifications.cloudflare_missing_zone', ['domain' => $this->domain?->name ?? 'unknown', 'subdomain' => $this->name . '.' . ($this->domain?->name ?? 'unknown')])) ->send(); return; @@ -107,14 +108,14 @@ protected function upsertOnCloudflare(): void // SRV: target comes from node, port from server allocation if ($this->record_type === 'SRV') { - $port = $this->server && $this->server->allocation ? ($this->server->allocation->port ?? null) : null; + $port = $this->server?->allocation?->port ?? null; if (empty($port)) { Log::warning('Server missing allocation with port', $this->toArray()); Notification::make() ->danger() ->title(trans('subdomains::strings.notifications.cloudflare_missing_srv_port_title')) - ->body(trans('subdomains::strings.notifications.cloudflare_missing_srv_port', ['subdomain' => $this->name . '.' . ($this->domain->name ?? 'unknown')])) + ->body(trans('subdomains::strings.notifications.cloudflare_missing_srv_port', ['subdomain' => $this->name . '.' . ($this->domain?->name ?? 'unknown')])) ->send(); return; @@ -122,22 +123,22 @@ protected function upsertOnCloudflare(): void $serviceRecordType = ServiceRecordType::fromServer($this->server); if (!$serviceRecordType) { - Log::warning('Unable to determine service record type for SRV record', ['server_id' => $this->server->id, 'server' => $this->server->name]); + Log::warning('Unable to determine service record type for SRV record', ['server_id' => $this->server?->id, 'server' => $this->server?->name]); Notification::make() ->danger() ->title(trans('subdomains::strings.notifications.cloudflare_invalid_service_record_type_title')) - ->body(trans('subdomains::strings.notifications.cloudflare_invalid_service_record_type', ['subdomain' => $this->name . '.' . ($this->domain->name ?? 'unknown')])) + ->body(trans('subdomains::strings.notifications.cloudflare_invalid_service_record_type', ['subdomain' => $this->name . '.' . ($this->domain?->name ?? 'unknown')])) ->send(); return; } - if (!$this->server || !$this->server->node || empty($this->server->node->srv_target)) { + if (empty($this->server?->node?->srv_target)) { Log::warning('Node missing SRV target for SRV record', ['node_id' => $this->server->node?->id ?? null]); Notification::make() ->danger() ->title(trans('subdomains::strings.notifications.cloudflare_missing_srv_target_title')) - ->body(trans('subdomains::strings.notifications.cloudflare_missing_srv_target', ['node' => $this->server->node->name])) + ->body(trans('subdomains::strings.notifications.cloudflare_missing_srv_target', ['node' => $this->server->node?->name ?? 'unknown'])) ->send(); return; @@ -171,7 +172,8 @@ protected function upsertOnCloudflare(): void } // A/AAAA - if (!$this->server || !$this->server->allocation || $this->server->allocation->ip === '0.0.0.0' || $this->server->allocation->ip === '::') { + $ip = $this->server?->allocation?->ip; + if (empty($ip) || $ip === '0.0.0.0' || $ip === '::') { Log::warning('Server allocation missing or invalid IP', ['server_id' => $this->server_id]); Notification::make() @@ -183,7 +185,7 @@ protected function upsertOnCloudflare(): void return; } - $result = $registrar->upsertDnsRecord($zoneId, $this->name, $this->record_type, $this->server->allocation->ip, $this->cloudflare_id, null); + $result = $registrar->upsertDnsRecord($zoneId, $this->name, $this->record_type, $ip, $this->cloudflare_id, null); if ($result['success'] && !empty($result['id'])) { if ($this->cloudflare_id !== $result['id']) { From ccaa94eaef417eed5b4b1b599136a8334a83f2c2 Mon Sep 17 00:00:00 2001 From: HarlequinSin Date: Thu, 1 Jan 2026 21:11:21 +1100 Subject: [PATCH 43/51] more simplified npe checks --- subdomains/src/Models/Subdomain.php | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/subdomains/src/Models/Subdomain.php b/subdomains/src/Models/Subdomain.php index 5da74ea..4b1463f 100644 --- a/subdomains/src/Models/Subdomain.php +++ b/subdomains/src/Models/Subdomain.php @@ -80,7 +80,7 @@ public function setSrvRecordAttribute($isSrvRecord): void $this->attributes['record_type'] = 'SRV'; } else { $ip = $this->server?->allocation?->ip ?? null; - if (!empty($ip) && filter_var($ip, FILTER_FLAG_IPV6)) { + if (!empty($ip) && filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) { $this->attributes['record_type'] = 'AAAA'; } else { $this->attributes['record_type'] = 'A'; @@ -156,7 +156,7 @@ protected function upsertOnCloudflare(): void Notification::make() ->success() ->title(trans('subdomains::strings.notifications.cloudflare_record_updated_title')) - ->body(trans('subdomains::strings.notifications.cloudflare_record_updated', ['subdomain' => $this->name . '.' . ($this->domain->name ?? 'unknown'), 'record_type' => $this->record_type])) + ->body(trans('subdomains::strings.notifications.cloudflare_record_updated', ['subdomain' => $this->name . '.' . ($this->domain?->name ?? 'unknown'), 'record_type' => $this->record_type])) ->send(); } else { Log::error('Failed to upsert SRV record on Cloudflare for Subdomain ID ' . $this->id, ['result' => $result]); @@ -164,7 +164,7 @@ protected function upsertOnCloudflare(): void Notification::make() ->danger() ->title(trans('subdomains::strings.notifications.cloudflare_upsert_failed_title')) - ->body(trans('subdomains::strings.notifications.cloudflare_upsert_failed', ['subdomain' => $this->name . '.' . ($this->domain->name ?? 'unknown'), 'errors' => json_encode($result['errors'] ?? $result['body'] ?? [])])) + ->body(trans('subdomains::strings.notifications.cloudflare_upsert_failed', ['subdomain' => $this->name . '.' . ($this->domain?->name ?? 'unknown'), 'errors' => json_encode($result['errors'] ?? $result['body'] ?? [])])) ->send(); } @@ -172,14 +172,14 @@ protected function upsertOnCloudflare(): void } // A/AAAA - $ip = $this->server?->allocation?->ip; + $ip = $this->server?->allocation?->ip ?? null; if (empty($ip) || $ip === '0.0.0.0' || $ip === '::') { Log::warning('Server allocation missing or invalid IP', ['server_id' => $this->server_id]); Notification::make() ->danger() ->title(trans('subdomains::strings.notifications.cloudflare_missing_ip_title')) - ->body(trans('subdomains::strings.notifications.cloudflare_missing_ip', ['subdomain' => $this->name . '.' . ($this->domain->name ?? 'unknown')])) + ->body(trans('subdomains::strings.notifications.cloudflare_missing_ip', ['subdomain' => $this->name . '.' . ($this->domain?->name ?? 'unknown')])) ->send(); return; @@ -195,12 +195,12 @@ protected function upsertOnCloudflare(): void Notification::make() ->success() ->title(trans('subdomains::strings.notifications.cloudflare_record_updated_title')) - ->body(trans('subdomains::strings.notifications.cloudflare_record_updated', ['subdomain' => $this->name . '.' . ($this->domain->name ?? 'unknown'), 'record_type' => $this->record_type])) + ->body(trans('subdomains::strings.notifications.cloudflare_record_updated', ['subdomain' => $this->name . '.' . ($this->domain?->name ?? 'unknown'), 'record_type' => $this->record_type])) ->send(); } else { Log::error('Failed to upsert record on Cloudflare for Subdomain ID ' . $this->id, ['result' => $result]); - $domainName = $this->domain->name ?? 'unknown'; + $domainName = $this->domain?->name ?? 'unknown'; $sub = sprintf('%s.%s', $this->name, $domainName); Notification::make() @@ -222,13 +222,13 @@ protected function deleteOnCloudflare(): void Notification::make() ->success() ->title(trans('subdomains::strings.notifications.cloudflare_delete_success_title')) - ->body(trans('subdomains::strings.notifications.cloudflare_delete_success', ['subdomain' => $this->name . '.' . ($this->domain->name ?? 'unknown')])) + ->body(trans('subdomains::strings.notifications.cloudflare_delete_success', ['subdomain' => $this->name . '.' . ($this->domain?->name ?? 'unknown')])) ->send(); } else { Notification::make() ->danger() ->title(trans('subdomains::strings.notifications.cloudflare_delete_failed_title')) - ->body(trans('subdomains::strings.notifications.cloudflare_delete_failed', ['subdomain' => $this->name . '.' . ($this->domain->name ?? 'unknown'), 'errors' => json_encode($result['errors'] ?? $result['body'] ?? [])])) + ->body(trans('subdomains::strings.notifications.cloudflare_delete_failed', ['subdomain' => $this->name . '.' . ($this->domain?->name ?? 'unknown'), 'errors' => json_encode($result['errors'] ?? $result['body'] ?? [])])) ->send(); } From f4757fdb94d14059e83eecccf12fd69a6f0aa3b1 Mon Sep 17 00:00:00 2001 From: HarlequinSin Date: Thu, 1 Jan 2026 21:15:17 +1100 Subject: [PATCH 44/51] npe and pint style issues --- subdomains/src/Filament/Admin/Nodes/Pages/EditNode.php | 1 - subdomains/src/Services/CloudflareService.php | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/subdomains/src/Filament/Admin/Nodes/Pages/EditNode.php b/subdomains/src/Filament/Admin/Nodes/Pages/EditNode.php index cfe48d4..250fff9 100644 --- a/subdomains/src/Filament/Admin/Nodes/Pages/EditNode.php +++ b/subdomains/src/Filament/Admin/Nodes/Pages/EditNode.php @@ -34,7 +34,6 @@ use Filament\Support\RawJs; use Illuminate\Http\Client\ConnectionException; use Illuminate\Support\Facades\Http; -use Illuminate\Support\Facades\Log; use Illuminate\Support\HtmlString; use Phiki\Grammar\Grammar; diff --git a/subdomains/src/Services/CloudflareService.php b/subdomains/src/Services/CloudflareService.php index ab19184..1781378 100644 --- a/subdomains/src/Services/CloudflareService.php +++ b/subdomains/src/Services/CloudflareService.php @@ -124,7 +124,7 @@ protected function parseCloudflareHttpResponse(Response $response): array $status = $response->status(); $body = $response->json() ?? []; - $success = $response->successful() && ($body['success'] === true || count($body['result']) > 0); + $success = $response->successful() && ($body['success'] === true || (is_array($body['result']) && count($body['result']) > 0)); return [ 'success' => $success, From 060dd6e99d71a11837b04702708b17dbe3bcc3f4 Mon Sep 17 00:00:00 2001 From: HarlequinSin Date: Fri, 2 Jan 2026 07:43:29 +1100 Subject: [PATCH 45/51] Added field updated check for srv_target --- subdomains/src/Filament/Admin/Nodes/Pages/EditNode.php | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/subdomains/src/Filament/Admin/Nodes/Pages/EditNode.php b/subdomains/src/Filament/Admin/Nodes/Pages/EditNode.php index 250fff9..4f6f9c3 100644 --- a/subdomains/src/Filament/Admin/Nodes/Pages/EditNode.php +++ b/subdomains/src/Filament/Admin/Nodes/Pages/EditNode.php @@ -36,6 +36,7 @@ use Illuminate\Support\Facades\Http; use Illuminate\Support\HtmlString; use Phiki\Grammar\Grammar; +use Throwable; class EditNode extends BaseEditNode { @@ -376,8 +377,11 @@ public function form(Schema $schema): Schema 'lg' => 3, ]) ->dehydrateStateUsing(function ($state) { - $this->record->forceFill(['srv_target' => $state])->save(); + if ($this->record->srv_target === $state) { + return $state; + } + $this->record->forceFill(['srv_target' => $state])->save(); Notification::make() ->title(trans('subdomains::strings.notifications.srv_target_updated_title')) ->body(trans('subdomains::strings.notifications.srv_target_updated')) @@ -386,6 +390,7 @@ public function form(Schema $schema): Schema return $state; }), + // Subdomains plugin field Grid::make() ->columns([ 'default' => 1, From fa0e3a6446a7d21180c92d549f3be66ecbea50e1 Mon Sep 17 00:00:00 2001 From: HarlequinSin Date: Fri, 2 Jan 2026 15:01:04 +1100 Subject: [PATCH 46/51] creating set srv_target button --- .../Components/Actions/SetSrvTargetAction.php | 48 +++++++++++++++++++ .../Providers/SubdomainsPluginProvider.php | 4 +- 2 files changed, 51 insertions(+), 1 deletion(-) create mode 100644 subdomains/src/Filament/Components/Actions/SetSrvTargetAction.php diff --git a/subdomains/src/Filament/Components/Actions/SetSrvTargetAction.php b/subdomains/src/Filament/Components/Actions/SetSrvTargetAction.php new file mode 100644 index 0000000..ef1feb7 --- /dev/null +++ b/subdomains/src/Filament/Components/Actions/SetSrvTargetAction.php @@ -0,0 +1,48 @@ +label(fn () => trans('subdomains::strings.set_srv_target')); + + $this->icon('tabler-world-www'); + + $this->schema(function (?Node $node) { + return [ + TextInput::make('srv_target') + ->label(fn () => trans('subdomains::strings.srv_target')) + ->default(fn () => $node?->srv_target) + ->placeholder('play.example.com OR IPv4/IPv6 address') + ->helperText(trans('subdomains::strings.srv_target_confirmation')) + - requireConfirmation() + ->rules(['nullable', 'string', 'regex:/^(?=.{1,253}$)(?!-)[A-Za-z0-9-]{1,63}(?action(function (Node $node, array $data) { + // ForceFill so we don't need to overwrite on Node::$fillable + $node->forceFill(['srv_target' => $data['srv_target']])->save(); + + Notification::make() + ->title(trans('subdomains::strings.notifications.srv_target_updated_title')) + ->body(trans('subdomains::strings.notifications.srv_target_updated')) + ->warning() + ->send(); + })->requiresConfirmation()->modalIconColor('danger')->modalDescription(trans('subdomains::strings.srv_target_confirmation')->toString()); + } +} diff --git a/subdomains/src/Providers/SubdomainsPluginProvider.php b/subdomains/src/Providers/SubdomainsPluginProvider.php index 4bd2198..d1cb0b7 100644 --- a/subdomains/src/Providers/SubdomainsPluginProvider.php +++ b/subdomains/src/Providers/SubdomainsPluginProvider.php @@ -2,11 +2,13 @@ namespace Boy132\Subdomains\Providers; +use App\Enums\HeaderActionPosition; use App\Filament\Admin\Resources\Nodes\NodeResource; use App\Filament\Admin\Resources\Servers\ServerResource; use App\Models\Role; use App\Models\Server; use Boy132\Subdomains\Filament\Admin\Resources\Nodes\Pages\EditNode; +use Boy132\Subdomains\Filament\Components\Actions\SetSrvTargetAction; use Boy132\Subdomains\Models\Subdomain; use Illuminate\Support\Facades\Http; use Illuminate\Support\ServiceProvider; @@ -15,8 +17,8 @@ class SubdomainsPluginProvider extends ServiceProvider { public function register(): void { - NodeResource::registerCustomPages(['edit' => EditNode::route('/{record}/edit')]); ServerResource::registerCustomRelations(SubdomainRelationManager::class); + EditNode::registerCustomHeaderActions(HeaderActionPosition::Before, SetSrvTargetAction::make()); Role::registerCustomDefaultPermissions('cloudflare_domain'); Role::registerCustomModelIcon('cloudflare_domain', 'tabler-world-www'); From 565adf6085f1613a7097b245bc09bd8401599fed Mon Sep 17 00:00:00 2001 From: HarlequinSin Date: Fri, 2 Jan 2026 15:04:10 +1100 Subject: [PATCH 47/51] changing subdomain model logic to: Update registrar, if success, update DB --- subdomains/src/Models/Subdomain.php | 145 +++++++++++++++++----------- 1 file changed, 86 insertions(+), 59 deletions(-) diff --git a/subdomains/src/Models/Subdomain.php b/subdomains/src/Models/Subdomain.php index 4b1463f..b24a4c2 100644 --- a/subdomains/src/Models/Subdomain.php +++ b/subdomains/src/Models/Subdomain.php @@ -5,6 +5,7 @@ use App\Models\Server; use Boy132\Subdomains\Enums\ServiceRecordType; use Boy132\Subdomains\Services\CloudflareService; +use Filament\Facades\Filament; use Filament\Notifications\Notification; use Filament\Support\Contracts\HasLabel; use Illuminate\Contracts\Support\Htmlable; @@ -45,12 +46,52 @@ protected static function boot(): void { parent::boot(); - static::saved(function (self $model) { - $model->upsertOnCloudflare(); + static::creating(function (self $model) { + // Relation does not exist yet, so we need to set it manually. + $model->setRelation('server', Filament::getTenant()); + + $registrarUpdated = $model->upsertOnCloudflare(); + if (!$registrarUpdated) { + return false; + } + + Notification::make() + ->success() + ->title(trans('subdomains::strings.notifications.cloudflare_record_created_title')) + ->body(trans('subdomains::strings.notifications.cloudflare_record_created', ['subdomain' => $model->name . '.' . ($model->domain?->name ?? 'unknown'), 'record_type' => $model->record_type])) + ->send(); + + return true; }); - static::deleted(function (self $model) { - $model->deleteOnCloudflare(); + static::updating(function (self $model) { + $registrarUpdated = $model->upsertOnCloudflare(); + if (!$registrarUpdated) { + return false; + } + + Notification::make() + ->success() + ->title(trans('subdomains::strings.notifications.cloudflare_record_updated_title')) + ->body(trans('subdomains::strings.notifications.cloudflare_record_updated', ['subdomain' => $model->name . '.' . ($model->domain?->name ?? 'unknown'), 'record_type' => $model->record_type])) + ->send(); + + return true; + }); + + static::deleting(function (self $model) { + $registrarUpdated = $model->deleteOnCloudflare(); + if (!$registrarUpdated) { + return false; + } + + Notification::make() + ->success() + ->title(trans('subdomains::strings.notifications.cloudflare_record_deleted_title')) + ->body(trans('subdomains::strings.notifications.cloudflare_record_deleted', ['subdomain' => $model->name . '.' . ($model->domain?->name ?? 'unknown')])) + ->send(); + + return true; }); } @@ -88,11 +129,10 @@ public function setSrvRecordAttribute($isSrvRecord): void } } - protected function upsertOnCloudflare(): void + protected function upsertOnCloudflare(): bool { $registrar = app(CloudflareService::class); - // add null check for domain $zoneId = $this->domain?->cloudflare_id ?? null; if (empty($zoneId)) { Log::warning('Cloudflare zone id missing for domain', ['domain_id' => $this->domain_id]); @@ -103,22 +143,21 @@ protected function upsertOnCloudflare(): void ->body(trans('subdomains::strings.notifications.cloudflare_missing_zone', ['domain' => $this->domain?->name ?? 'unknown', 'subdomain' => $this->name . '.' . ($this->domain?->name ?? 'unknown')])) ->send(); - return; + return false; } // SRV: target comes from node, port from server allocation if ($this->record_type === 'SRV') { - $port = $this->server?->allocation?->port ?? null; - + $port = $this->server->allocation?->port ?? null; if (empty($port)) { Log::warning('Server missing allocation with port', $this->toArray()); Notification::make() ->danger() ->title(trans('subdomains::strings.notifications.cloudflare_missing_srv_port_title')) - ->body(trans('subdomains::strings.notifications.cloudflare_missing_srv_port', ['subdomain' => $this->name . '.' . ($this->domain?->name ?? 'unknown')])) + ->body(trans('subdomains::strings.notifications.cloudflare_missing_srv_port', ['server' => $this->server?->name ?? 'unassigned'])) ->send(); - return; + return false; } $serviceRecordType = ServiceRecordType::fromServer($this->server); @@ -130,7 +169,7 @@ protected function upsertOnCloudflare(): void ->body(trans('subdomains::strings.notifications.cloudflare_invalid_service_record_type', ['subdomain' => $this->name . '.' . ($this->domain?->name ?? 'unknown')])) ->send(); - return; + return false; } if (empty($this->server?->node?->srv_target)) { @@ -141,48 +180,41 @@ protected function upsertOnCloudflare(): void ->body(trans('subdomains::strings.notifications.cloudflare_missing_srv_target', ['node' => $this->server->node?->name ?? 'unknown'])) ->send(); - return; + return false; } $recordName = sprintf('%s.%s.%s', $serviceRecordType->service(), $serviceRecordType->protocol(), $this->name); - $result = $registrar->upsertDnsRecord($zoneId, $recordName, 'SRV', $this->server->node->srv_target, $this->cloudflare_id, $port, $serviceRecordType); + $result = $registrar->upsertDnsRecord($zoneId, $recordName, 'SRV', $this->server->node->srv_target, $this->cloudflare_id, $port); if ($result['success'] && !empty($result['id'])) { if ($this->cloudflare_id !== $result['id']) { $this->updateQuietly(['cloudflare_id' => $result['id']]); } - Notification::make() - ->success() - ->title(trans('subdomains::strings.notifications.cloudflare_record_updated_title')) - ->body(trans('subdomains::strings.notifications.cloudflare_record_updated', ['subdomain' => $this->name . '.' . ($this->domain?->name ?? 'unknown'), 'record_type' => $this->record_type])) - ->send(); - } else { - Log::error('Failed to upsert SRV record on Cloudflare for Subdomain ID ' . $this->id, ['result' => $result]); - - Notification::make() - ->danger() - ->title(trans('subdomains::strings.notifications.cloudflare_upsert_failed_title')) - ->body(trans('subdomains::strings.notifications.cloudflare_upsert_failed', ['subdomain' => $this->name . '.' . ($this->domain?->name ?? 'unknown'), 'errors' => json_encode($result['errors'] ?? $result['body'] ?? [])])) - ->send(); + return true; } + Log::error('Failed to upsert SRV record on Cloudflare for Subdomain ID ' . $this->id, ['result' => $result]); + Notification::make() + ->danger() + ->title(trans('subdomains::strings.notifications.cloudflare_upsert_failed_title')) + ->body(trans('subdomains::strings.notifications.cloudflare_upsert_failed', ['subdomain' => $this->name . '.' . ($this->domain?->name ?? 'unknown'), 'errors' => json_encode($result['errors'] ?? $result['body'] ?? [])])) + ->send(); - return; + return false; } // A/AAAA $ip = $this->server?->allocation?->ip ?? null; if (empty($ip) || $ip === '0.0.0.0' || $ip === '::') { Log::warning('Server allocation missing or invalid IP', ['server_id' => $this->server_id]); - Notification::make() ->danger() ->title(trans('subdomains::strings.notifications.cloudflare_missing_ip_title')) ->body(trans('subdomains::strings.notifications.cloudflare_missing_ip', ['subdomain' => $this->name . '.' . ($this->domain?->name ?? 'unknown')])) ->send(); - return; + return false; } $result = $registrar->upsertDnsRecord($zoneId, $this->name, $this->record_type, $ip, $this->cloudflare_id, null); @@ -192,47 +224,42 @@ protected function upsertOnCloudflare(): void $this->updateQuietly(['cloudflare_id' => $result['id']]); } - Notification::make() - ->success() - ->title(trans('subdomains::strings.notifications.cloudflare_record_updated_title')) - ->body(trans('subdomains::strings.notifications.cloudflare_record_updated', ['subdomain' => $this->name . '.' . ($this->domain?->name ?? 'unknown'), 'record_type' => $this->record_type])) - ->send(); - } else { - Log::error('Failed to upsert record on Cloudflare for Subdomain ID ' . $this->id, ['result' => $result]); + return true; + } - $domainName = $this->domain?->name ?? 'unknown'; - $sub = sprintf('%s.%s', $this->name, $domainName); + $domainName = $this->domain?->name ?? 'unknown'; + $subdomain = sprintf('%s.%s', $this->name, $domainName); - Notification::make() - ->danger() - ->title(trans('subdomains::strings.notifications.cloudflare_upsert_failed_title')) - ->body(trans('subdomains::strings.notifications.cloudflare_upsert_failed', ['subdomain' => $sub, 'errors' => json_encode($result['errors'] ?? $result['body'] ?? [])])) - ->send(); - } + Log::error('Failed to upsert record on Cloudflare for Subdomain ID ' . $this->id, ['result' => $result]); + Notification::make() + ->danger() + ->title(trans('subdomains::strings.notifications.cloudflare_upsert_failed_title')) + ->body(trans('subdomains::strings.notifications.cloudflare_upsert_failed', ['subdomain' => $subdomain, 'errors' => json_encode($result['errors'] ?? $result['body'] ?? [])])) + ->send(); + + return false; } - protected function deleteOnCloudflare(): void + protected function deleteOnCloudflare(): bool { if ($this->cloudflare_id && $this->domain && $this->domain->cloudflare_id) { $registrar = app(CloudflareService::class); $result = $registrar->deleteDnsRecord($this->domain->cloudflare_id, $this->cloudflare_id); - if (!empty($result['success'])) { - Notification::make() - ->success() - ->title(trans('subdomains::strings.notifications.cloudflare_delete_success_title')) - ->body(trans('subdomains::strings.notifications.cloudflare_delete_success', ['subdomain' => $this->name . '.' . ($this->domain?->name ?? 'unknown')])) - ->send(); - } else { - Notification::make() - ->danger() - ->title(trans('subdomains::strings.notifications.cloudflare_delete_failed_title')) - ->body(trans('subdomains::strings.notifications.cloudflare_delete_failed', ['subdomain' => $this->name . '.' . ($this->domain?->name ?? 'unknown'), 'errors' => json_encode($result['errors'] ?? $result['body'] ?? [])])) - ->send(); + if (!empty($result['success']) || $result['status'] === 404) { + return true; } - return; + Notification::make() + ->danger() + ->title(trans('subdomains::strings.notifications.cloudflare_delete_failed_title')) + ->body(trans('subdomains::strings.notifications.cloudflare_delete_failed', ['subdomain' => $this->name . '.' . ($this->domain?->name ?? 'unknown'), 'errors' => json_encode($result['errors'] ?? $result['body'] ?? [])])) + ->send(); + + return false; } + + return true; } } From 912984c1c99dd71eb69921a86b84f24b5e1d75fb Mon Sep 17 00:00:00 2001 From: HarlequinSin Date: Fri, 2 Jan 2026 15:04:52 +1100 Subject: [PATCH 48/51] fixing notification issues --- subdomains/lang/en/strings.php | 14 +++--- subdomains/src/Services/CloudflareService.php | 47 +++++++++---------- 2 files changed, 30 insertions(+), 31 deletions(-) diff --git a/subdomains/lang/en/strings.php b/subdomains/lang/en/strings.php index 6ad3ff7..701cbe5 100644 --- a/subdomains/lang/en/strings.php +++ b/subdomains/lang/en/strings.php @@ -24,7 +24,7 @@ 'srv_target' => 'SRV Target', 'srv_target_help' => 'The hostname that SRV records point to (for example: play.example.com).', 'srv_target_missing' => 'SRV record is missing from node configuration.', - 'srv_target_confirmation' => 'Changing the SRV target will require all existing subdomains using SRV records to be updated. Are you sure you want to continue?', + 'srv_target_confirmation' => 'Changing the SRV target will require all existing subdomains using SRV records to be updated to reflect the new target.', 'api_token' => 'Cloudflare API Token', 'api_token_help' => 'The token needs to have read permissions for Zone.Zone and write for Zone.Dns. For better security you can also set the "Zone Resources" to exclude certain domains and add the panel ip to the "Client IP Adress Filtering".', @@ -42,21 +42,21 @@ 'cloudflare_missing_srv_target_title' => 'Cloudflare: Missing SRV Target', 'cloudflare_missing_srv_target' => 'SRV target is missing from :node. ', + 'cloudflare_record_created_title' => 'Cloudflare: Record Created', + 'cloudflare_record_created' => 'Successfully created :subdomain record of type :record', + 'cloudflare_record_updated_title' => 'Cloudflare: Record Updated', 'cloudflare_record_updated' => 'Successfully updated :subdomain record to :record_type', + 'cloudflare_record_deleted_title' => 'Cloudflare: Record Deleted', + 'cloudflare_record_deleted' => 'Successfully deleted Cloudflare record for :subdomain.', + 'cloudflare_missing_ip_title' => 'Cloudflare: Missing IP', 'cloudflare_missing_ip' => 'Server allocation IP is missing or invalid for :subdomain. Cannot save A/AAAA record.', 'cloudflare_upsert_failed_title' => 'Cloudflare: Save Failed', 'cloudflare_upsert_failed' => 'Failed to save record for :subdomain. See logs for details. Errors: :errors', - 'cloudflare_delete_success_title' => 'Cloudflare: Record Deleted', - 'cloudflare_delete_success' => 'Successfully deleted Cloudflare record for :subdomain.', - - 'cloudflare_delete_failed_title' => 'Cloudflare: Delete Failed', - 'cloudflare_delete_failed' => 'Failed to delete Cloudflare record for :subdomain. See logs for details. Errors: :errors', - 'cloudflare_zone_fetch_failed' => 'Failed to fetch Cloudflare Zone ID for domain: :domain', 'cloudflare_domain_saved' => 'Successfully saved domain: :domain', diff --git a/subdomains/src/Services/CloudflareService.php b/subdomains/src/Services/CloudflareService.php index 1781378..af8cc9a 100644 --- a/subdomains/src/Services/CloudflareService.php +++ b/subdomains/src/Services/CloudflareService.php @@ -23,10 +23,9 @@ public function getZoneId(string $domainName): ?string } catch (\Throwable $e) { Log::error('Cloudflare getZoneId request failed: ' . $e->getMessage(), ['domain' => $domainName]); - return null; + return ['success' => false, 'errors' => ['exception' => $e->getMessage()], 'status' => $e->getCode(), 'body' => $e->getTraceAsString()]; } - $status = $response->status(); $body = $response->json() ?? []; if ($response->successful() && !empty($body['result']) && count($body['result']) > 0) { @@ -34,10 +33,10 @@ public function getZoneId(string $domainName): ?string } if (!empty($body['errors'])) { - Log::warning('Cloudflare getZoneId returned errors', ['domain' => $domainName, 'status' => $status, 'errors' => $body['errors']]); + Log::warning('Cloudflare getZoneId returned errors', ['domain' => $domainName, 'status' => $response->status(), 'errors' => $body['errors']]); } - return null; + return ['success' => false, 'errors' => [], 'status' => $response->status(), 'body' => $body]; } public function upsertDnsRecord(string $zoneId, string $name, string $recordType, string $target, ?string $recordId = null, ?int $port = null): array @@ -113,28 +112,12 @@ public function upsertDnsRecord(string $zoneId, string $name, string $recordType return $parsed; } catch (\Throwable $e) { - Log::error('Cloudflare upsert exception: ' . $e->getMessage(), ['zone' => $zoneId, 'payload' => $payload]); + Log::error('Cloudflare upsert exception: ' . $e->getMessage(), ['zone' => $zoneId, 'payload' => $payload, 'status' => $e->getCode()]); - return ['success' => false, 'id' => null, 'errors' => ['exception' => $e->getMessage()], 'status' => 0, 'body' => null]; + return ['success' => false, 'errors' => ['exception' => $e->getMessage()], 'status' => $e->getCode(), 'body' => $e->getTraceAsString()]; } } - protected function parseCloudflareHttpResponse(Response $response): array - { - $status = $response->status(); - $body = $response->json() ?? []; - - $success = $response->successful() && ($body['success'] === true || (is_array($body['result']) && count($body['result']) > 0)); - - return [ - 'success' => $success, - 'id' => $body['result']['id'] ?? null, - 'errors' => $body['errors'] ?? [], - 'status' => $status, - 'body' => $body, - ]; - } - public function deleteDnsRecord(string $zoneId, string $recordId): array { if (empty($zoneId) || empty($recordId)) { @@ -154,9 +137,25 @@ public function deleteDnsRecord(string $zoneId, string $recordId): array return $parsed; } catch (\Throwable $e) { - Log::error('Cloudflare delete exception: ' . $e->getMessage(), ['zone' => $zoneId, 'id' => $recordId]); + Log::error('Cloudflare delete exception: ' . $e->getMessage(), ['zone' => $zoneId, 'id' => $recordId, 'payload' => $payload, 'status' => $e->getCode()]); - return ['success' => false, 'errors' => ['exception' => $e->getMessage()], 'status' => 0, 'body' => null]; + return ['success' => false, 'errors' => ['exception' => $e->getMessage()], 'status' => $e->getCode(), 'body' => $e->getTraceAsString()]; } } + + protected function parseCloudflareHttpResponse(Response $response): array + { + $status = $response->status(); + $body = $response->json() ?? []; + + $success = $response->successful() && ($body['success'] === true || (is_array($body['result']) && count($body['result']) > 0)); + + return [ + 'success' => $success, + 'id' => $body['result']['id'] ?? null, + 'errors' => $body['errors'] ?? [], + 'status' => $status, + 'body' => $body, + ]; + } } From c771faa9bbe2c3e0d43471a7349ccabc20cffef0 Mon Sep 17 00:00:00 2001 From: HarlequinSin Date: Fri, 2 Jan 2026 15:39:27 +1100 Subject: [PATCH 49/51] moved to button --- .../Filament/Admin/Nodes/Pages/EditNode.php | 786 ------------------ 1 file changed, 786 deletions(-) delete mode 100644 subdomains/src/Filament/Admin/Nodes/Pages/EditNode.php diff --git a/subdomains/src/Filament/Admin/Nodes/Pages/EditNode.php b/subdomains/src/Filament/Admin/Nodes/Pages/EditNode.php deleted file mode 100644 index 4f6f9c3..0000000 --- a/subdomains/src/Filament/Admin/Nodes/Pages/EditNode.php +++ /dev/null @@ -1,786 +0,0 @@ -components([ - Tabs::make('Tabs') - ->columns([ - 'default' => 2, - 'sm' => 3, - 'md' => 3, - 'lg' => 4, - ]) - ->persistTabInQueryString() - ->columnSpanFull() - ->tabs([ - Tab::make('overview') - ->label(trans('admin/node.tabs.overview')) - ->icon('tabler-chart-area-line-filled') - ->columns([ - 'default' => 4, - 'sm' => 2, - 'md' => 4, - 'lg' => 4, - ]) - ->schema([ - Fieldset::make() - ->label(trans('admin/node.node_info')) - ->columns(4) - ->columnSpanFull() - ->schema([ - TextEntry::make('wings_version') - ->label(trans('admin/node.wings_version')) - ->state(fn (Node $node, SoftwareVersionService $versionService) => ($node->systemInformation()['version'] ?? trans('admin/node.unknown')) . ' ' . trans('admin/node.latest', ['version' => $versionService->latestWingsVersion()])), - TextEntry::make('cpu_threads') - ->label(trans('admin/node.cpu_threads')) - ->state(fn (Node $node) => $node->systemInformation()['cpu_count'] ?? 0), - TextEntry::make('architecture') - ->label(trans('admin/node.architecture')) - ->state(fn (Node $node) => $node->systemInformation()['architecture'] ?? trans('admin/node.unknown')), - TextEntry::make('kernel') - ->label(trans('admin/node.kernel')) - ->state(fn (Node $node) => $node->systemInformation()['kernel_version'] ?? trans('admin/node.unknown')), - ]), - View::make('filament.components.node-cpu-chart') - ->columnSpan([ - 'default' => 4, - 'sm' => 1, - 'md' => 2, - 'lg' => 2, - ]), - View::make('filament.components.node-memory-chart') - ->columnSpan([ - 'default' => 4, - 'sm' => 1, - 'md' => 2, - 'lg' => 2, - ]), - View::make('filament.components.node-storage-chart') - ->columnSpanFull(), - ]), - Tab::make('basic_settings') - ->label(trans('admin/node.tabs.basic_settings')) - ->icon('tabler-server') - ->schema([ - TextInput::make('fqdn') - ->columnSpan(2) - ->required() - ->autofocus() - ->live(debounce: 1500) - ->rules(Node::getRulesForField('fqdn')) - ->prohibited(fn ($state) => is_ip($state) && request()->isSecure()) - ->label(fn ($state) => is_ip($state) ? trans('admin/node.ip_address') : trans('admin/node.domain')) - ->placeholder(fn ($state) => is_ip($state) ? '192.168.1.1' : 'node.example.com') - ->helperText(function ($state) { - if (is_ip($state)) { - if (request()->isSecure()) { - return trans('admin/node.fqdn_help'); - } - - return ''; - } - - return trans('admin/node.error'); - }) - ->hintColor('danger') - ->hint(function ($state) { - if (is_ip($state) && request()->isSecure()) { - return trans('admin/node.ssl_ip'); - } - - return ''; - }) - ->afterStateUpdated(function (Set $set, ?string $state) { - $set('dns', null); - $set('ip', null); - - [$subdomain] = str($state)->explode('.', 2); - if (!is_numeric($subdomain)) { - $set('name', $subdomain); - } - - if (!$state || is_ip($state)) { - $set('dns', null); - - return; - } - - $ip = get_ip_from_hostname($state); - if ($ip) { - $set('dns', true); - - $set('ip', $ip); - } else { - $set('dns', false); - } - }) - ->maxLength(255), - TextInput::make('ip') - ->disabled() - ->hidden(), - ToggleButtons::make('dns') - ->label(trans('admin/node.dns')) - ->helperText(trans('admin/node.dns_help')) - ->disabled() - ->inline() - ->default(null) - ->hint(fn (Get $get) => $get('ip')) - ->hintColor('success') - ->stateCast(new BooleanStateCast(false, true)) - ->options([ - 1 => trans('admin/node.valid'), - 0 => trans('admin/node.invalid'), - ]) - ->colors([ - 1 => 'success', - 0 => 'danger', - ]) - ->columnSpan(1), - TextInput::make('daemon_connect') - ->columnSpan(1) - ->label(fn (Get $get) => $get('connection') === 'https_proxy' ? trans('admin/node.connect_port') : trans('admin/node.port')) - ->helperText(fn (Get $get) => $get('connection') === 'https_proxy' ? trans('admin/node.connect_port_help') : trans('admin/node.port_help')) - ->minValue(1) - ->maxValue(65535) - ->default(8080) - ->required() - ->integer(), - TextInput::make('name') - ->label(trans('admin/node.display_name')) - ->columnSpan([ - 'default' => 1, - 'sm' => 1, - 'md' => 1, - 'lg' => 2, - ]) - ->required() - ->maxLength(100), - Hidden::make('scheme'), - Hidden::make('behind_proxy'), - ToggleButtons::make('connection') - ->label(trans('admin/node.ssl')) - ->columnSpan(1) - ->inline() - ->helperText(function (Get $get) { - if (request()->isSecure()) { - return new HtmlString(trans('admin/node.panel_on_ssl')); - } - - if (is_ip($get('fqdn'))) { - return trans('admin/node.ssl_help'); - } - - return ''; - }) - ->disableOptionWhen(fn (string $value) => $value === 'http' && request()->isSecure()) - ->options([ - 'http' => 'HTTP', - 'https' => 'HTTPS (SSL)', - 'https_proxy' => 'HTTPS with (reverse) proxy', - ]) - ->colors([ - 'http' => 'warning', - 'https' => 'success', - 'https_proxy' => 'success', - ]) - ->icons([ - 'http' => 'tabler-lock-open-off', - 'https' => 'tabler-lock', - 'https_proxy' => 'tabler-shield-lock', - ]) - ->formatStateUsing(fn (Get $get) => $get('scheme') === 'http' ? 'http' : ($get('behind_proxy') ? 'https_proxy' : 'https')) - ->live() - ->dehydrated(false) - ->afterStateUpdated(function ($state, Set $set) { - $set('scheme', $state === 'http' ? 'http' : 'https'); - $set('behind_proxy', $state === 'https_proxy'); - - $set('daemon_connect', $state === 'https_proxy' ? 443 : 8080); - $set('daemon_listen', 8080); - }), - TextInput::make('daemon_listen') - ->columnSpan(1) - ->label(trans('admin/node.listen_port')) - ->helperText(trans('admin/node.listen_port_help')) - ->minValue(1) - ->maxValue(65535) - ->default(8080) - ->required() - ->integer() - ->visible(fn (Get $get) => $get('connection') === 'https_proxy'), - ]), - Tab::make('advanced_settings') - ->label(trans('admin/node.tabs.advanced_settings')) - ->columns([ - 'default' => 1, - 'sm' => 1, - 'md' => 4, - 'lg' => 6, - ]) - ->icon('tabler-server-cog') - ->schema([ - TextInput::make('id') - ->label(trans('admin/node.node_id')) - ->columnSpan([ - 'default' => 1, - 'sm' => 1, - 'md' => 2, - 'lg' => 1, - ]) - ->disabled(), - TextInput::make('uuid') - ->columnSpan([ - 'default' => 1, - 'sm' => 1, - 'md' => 2, - 'lg' => 2, - ]) - ->label(trans('admin/node.node_uuid')) - ->hintCopy() - ->disabled(), - TagsInput::make('tags') - ->label(trans('admin/node.tags')) - ->placeholder('') - ->columnSpan([ - 'default' => 1, - 'sm' => 1, - 'md' => 2, - 'lg' => 2, - ]), - TextInput::make('upload_size') - ->columnSpan([ - 'default' => 1, - 'sm' => 1, - 'md' => 2, - 'lg' => 1, - ]) - ->label(trans('admin/node.upload_limit')) - ->hintIcon('tabler-question-mark', trans('admin/node.upload_limit_help.0') . trans('admin/node.upload_limit_help.1')) - ->numeric()->required() - ->minValue(1) - ->maxValue(1024) - ->suffix(config('panel.use_binary_prefix') ? 'MiB' : 'MB'), - TextInput::make('daemon_sftp') - ->columnSpan([ - 'default' => 1, - 'sm' => 1, - 'md' => 1, - 'lg' => 3, - ]) - ->label(trans('admin/node.sftp_port')) - ->minValue(1) - ->maxValue(65535) - ->default(2022) - ->required() - ->integer(), - TextInput::make('daemon_sftp_alias') - ->columnSpan([ - 'default' => 1, - 'sm' => 1, - 'md' => 1, - 'lg' => 3, - ]) - ->label(trans('admin/node.sftp_alias')) - ->helperText(trans('admin/node.sftp_alias_help')), - ToggleButtons::make('public') - ->columnSpan([ - 'default' => 1, - 'sm' => 1, - 'md' => 1, - 'lg' => 3, - ]) - ->label(trans('admin/node.use_for_deploy')) - ->inline() - ->stateCast(new BooleanStateCast(false, true)) - ->options([ - 1 => trans('admin/node.yes'), - 0 => trans('admin/node.no'), - ]) - ->colors([ - 1 => 'success', - 0 => 'danger', - ]), - ToggleButtons::make('maintenance_mode') - ->columnSpan([ - 'default' => 1, - 'sm' => 1, - 'md' => 1, - 'lg' => 3, - ]) - ->label(trans('admin/node.maintenance_mode')) - ->inline() - ->hintIcon('tabler-question-mark', trans('admin/node.maintenance_mode_help')) - ->stateCast(new BooleanStateCast(false, true)) - ->options([ - 1 => trans('admin/node.enabled'), - 0 => trans('admin/node.disabled'), - ]) - ->colors([ - 1 => 'danger', - 0 => 'success', - ]), - // Subdomains plugin field - TextInput::make('srv_target') - ->label(trans('subdomains::strings.srv_target')) - ->placeholder('play.example.com') - ->helperText(trans('subdomains::strings.srv_target_help')) - ->rules(['nullable', 'string', 'regex:/^(?=.{1,253}$)(?!-)[A-Za-z0-9-]{1,63}(?columnSpan([ - 'default' => 1, - 'sm' => 1, - 'md' => 1, - 'lg' => 3, - ]) - ->dehydrateStateUsing(function ($state) { - if ($this->record->srv_target === $state) { - return $state; - } - - $this->record->forceFill(['srv_target' => $state])->save(); - Notification::make() - ->title(trans('subdomains::strings.notifications.srv_target_updated_title')) - ->body(trans('subdomains::strings.notifications.srv_target_updated')) - ->warning() - ->send(); - - return $state; - }), - // Subdomains plugin field - Grid::make() - ->columns([ - 'default' => 1, - 'sm' => 1, - 'md' => 3, - 'lg' => 6, - ]) - ->columnSpanFull() - ->schema([ - ToggleButtons::make('unlimited_mem') - ->dehydrated() - ->label(trans('admin/node.memory'))->inlineLabel()->inline() - ->afterStateUpdated(fn (Set $set) => $set('memory', 0)) - ->afterStateUpdated(fn (Set $set) => $set('memory_overallocate', 0)) - ->formatStateUsing(fn (Get $get) => $get('memory') == 0) - ->live() - ->stateCast(new BooleanStateCast(false, true)) - ->options([ - 1 => trans('admin/node.unlimited'), - 0 => trans('admin/node.limited'), - ]) - ->colors([ - 1 => 'primary', - 0 => 'warning', - ]) - ->columnSpan([ - 'default' => 1, - 'sm' => 1, - 'md' => 1, - 'lg' => 2, - ]), - TextInput::make('memory') - ->dehydratedWhenHidden() - ->hidden(fn (Get $get) => $get('unlimited_mem')) - ->label(trans('admin/node.memory_limit'))->inlineLabel() - ->suffix(config('panel.use_binary_prefix') ? 'MiB' : 'MB') - ->required() - ->columnSpan([ - 'default' => 1, - 'sm' => 1, - 'md' => 1, - 'lg' => 2, - ]) - ->numeric() - ->minValue(0), - TextInput::make('memory_overallocate') - ->dehydratedWhenHidden() - ->label(trans('admin/node.overallocate'))->inlineLabel() - ->required() - ->hidden(fn (Get $get) => $get('unlimited_mem')) - ->columnSpan([ - 'default' => 1, - 'sm' => 1, - 'md' => 1, - 'lg' => 2, - ]) - ->numeric() - ->minValue(-1) - ->maxValue(100) - ->suffix('%'), - ]), - Grid::make() - ->columnSpanFull() - ->columns([ - 'default' => 1, - 'sm' => 1, - 'md' => 3, - 'lg' => 6, - ]) - ->schema([ - ToggleButtons::make('unlimited_disk') - ->dehydrated() - ->label(trans('admin/node.disk'))->inlineLabel()->inline() - ->live() - ->afterStateUpdated(fn (Set $set) => $set('disk', 0)) - ->afterStateUpdated(fn (Set $set) => $set('disk_overallocate', 0)) - ->formatStateUsing(fn (Get $get) => $get('disk') == 0) - ->stateCast(new BooleanStateCast(false, true)) - ->options([ - 1 => trans('admin/node.unlimited'), - 0 => trans('admin/node.limited'), - ]) - ->colors([ - 1 => 'primary', - 0 => 'warning', - ]) - ->columnSpan([ - 'default' => 1, - 'sm' => 1, - 'md' => 1, - 'lg' => 2, - ]), - TextInput::make('disk') - ->dehydratedWhenHidden() - ->hidden(fn (Get $get) => $get('unlimited_disk')) - ->label(trans('admin/node.disk_limit'))->inlineLabel() - ->suffix(config('panel.use_binary_prefix') ? 'MiB' : 'MB') - ->required() - ->columnSpan([ - 'default' => 1, - 'sm' => 1, - 'md' => 1, - 'lg' => 2, - ]) - ->numeric() - ->minValue(0), - TextInput::make('disk_overallocate') - ->dehydratedWhenHidden() - ->hidden(fn (Get $get) => $get('unlimited_disk')) - ->label(trans('admin/node.overallocate'))->inlineLabel() - ->columnSpan([ - 'default' => 1, - 'sm' => 1, - 'md' => 1, - 'lg' => 2, - ]) - ->required() - ->numeric() - ->minValue(-1) - ->maxValue(100) - ->suffix('%'), - ]), - Grid::make() - ->columns(6) - ->columnSpanFull() - ->schema([ - ToggleButtons::make('unlimited_cpu') - ->dehydrated() - ->label(trans('admin/node.cpu'))->inlineLabel()->inline() - ->live() - ->afterStateUpdated(fn (Set $set) => $set('cpu', 0)) - ->afterStateUpdated(fn (Set $set) => $set('cpu_overallocate', 0)) - ->formatStateUsing(fn (Get $get) => $get('cpu') == 0) - ->stateCast(new BooleanStateCast(false, true)) - ->options([ - 1 => trans('admin/node.unlimited'), - 0 => trans('admin/node.limited'), - ]) - ->colors([ - 1 => 'primary', - 0 => 'warning', - ]) - ->columnSpan(2), - TextInput::make('cpu') - ->dehydratedWhenHidden() - ->hidden(fn (Get $get) => $get('unlimited_cpu')) - ->label(trans('admin/node.cpu_limit'))->inlineLabel() - ->suffix('%') - ->required() - ->columnSpan(2) - ->numeric() - ->minValue(0), - TextInput::make('cpu_overallocate') - ->dehydratedWhenHidden() - ->hidden(fn (Get $get) => $get('unlimited_cpu')) - ->label(trans('admin/node.overallocate'))->inlineLabel() - ->columnSpan(2) - ->required() - ->numeric() - ->minValue(-1) - ->maxValue(100) - ->suffix('%'), - ]), - ]), - Tab::make('config_file') - ->label(trans('admin/node.tabs.config_file')) - ->icon('tabler-code') - ->schema([ - TextEntry::make('instructions') - ->label(trans('admin/node.instructions')) - ->columnSpanFull() - ->state(new HtmlString(trans('admin/node.instructions_help'))), - CodeEntry::make('config') - ->label('/etc/pelican/config.yml') - ->grammar(Grammar::Yaml) - ->state(fn (Node $node) => $node->getYamlConfiguration()) - ->copyable() - ->disabled() - ->columnSpanFull(), - Grid::make() - ->columns() - ->columnSpanFull() - ->schema([ - Actions::make([ - Action::make('autoDeploy') - ->label(trans('admin/node.auto_deploy')) - ->color('primary') - ->modalHeading(trans('admin/node.auto_deploy')) - ->icon('tabler-rocket') - ->modalSubmitAction(false) - ->modalCancelAction(false) - ->modalFooterActionsAlignment(Alignment::Center) - ->schema([ - ToggleButtons::make('docker') - ->label(trans('admin/node.auto_label')) - ->live() - ->helperText(trans('admin/node.auto_question')) - ->inline() - ->default(false) - ->afterStateUpdated(fn (bool $state, NodeAutoDeployService $service, Node $node, Set $set) => $set('generatedToken', $service->handle(request(), $node, $state))) - ->stateCast(new BooleanStateCast(false, true)) - ->options([ - 0 => trans('admin/node.standalone'), - 1 => trans('admin/node.docker'), - ]) - ->colors([ - 0 => 'primary', - 1 => 'success', - ]) - ->columnSpan(1), - Textarea::make('generatedToken') - ->label(trans('admin/node.auto_command')) - ->readOnly() - ->autosize() - ->hintCopy() - ->formatStateUsing(fn (NodeAutoDeployService $service, Node $node, Set $set, Get $get) => $set('generatedToken', $service->handle(request(), $node, $get('docker')))), - ]) - ->mountUsing(function (Schema $schema) { - $schema->fill(); - }), - ])->fullWidth(), - Actions::make([ - Action::make('resetKey') - ->label(trans('admin/node.reset_token')) - ->color('danger') - ->requiresConfirmation() - ->modalHeading(trans('admin/node.reset_token')) - ->modalDescription(trans('admin/node.reset_help')) - ->action(function (Node $node) { - try { - $this->nodeUpdateService->handle($node, [], true); - } catch (Exception) { - Notification::make() - ->title(trans('admin/node.error_connecting', ['node' => $node->name])) - ->body(trans('admin/node.error_connecting_description')) - ->color('warning') - ->icon('tabler-database') - ->warning() - ->send(); - - } - Notification::make()->success()->title(trans('admin/node.token_reset'))->send(); - $this->fillForm(); - }), - ])->fullWidth(), - ]), - ]), - Tab::make('diagnostics') - ->label(trans('admin/node.tabs.diagnostics')) - ->icon('tabler-heart-search') - ->schema([ - Section::make('diag') - ->heading(trans('admin/node.tabs.diagnostics')) - ->columnSpanFull() - ->columns(4) - ->disabled(fn (Get $get) => $get('pulled')) - ->headerActions([ - Action::make('pull') - ->label(trans('admin/node.diagnostics.pull')) - ->icon('tabler-cloud-download')->iconButton()->iconSize(IconSize::ExtraLarge) - ->hidden(fn (Get $get) => $get('pulled')) - ->action(function (Get $get, Set $set, Node $node) { - $includeEndpoints = $get('include_endpoints') ?? true; - $includeLogs = $get('include_logs') ?? true; - $logLines = $get('log_lines') ?? 200; - - try { - $response = $this->daemonSystemRepository->setNode($node)->getDiagnostics($logLines, $includeEndpoints, $includeLogs); - - if ($response->status() === 404) { - Notification::make() - ->title(trans('admin/node.diagnostics.404')) - ->warning() - ->send(); - - return; - } - - $set('pulled', true); - $set('uploaded', false); - $set('log', $response->body()); - - Notification::make() - ->title(trans('admin/node.diagnostics.logs_pulled')) - ->success() - ->send(); - } catch (ConnectionException $e) { - Notification::make() - ->title(trans('admin/node.error_connecting', ['node' => $node->name])) - ->body($e->getMessage()) - ->danger() - ->send(); - - } - }), - Action::make('upload') - ->label(trans('admin/node.diagnostics.upload')) - ->visible(fn (Get $get) => $get('pulled') ?? false) - ->icon('tabler-cloud-upload')->iconButton()->iconSize(IconSize::ExtraLarge) - ->action(function (Get $get, Set $set) { - try { - $response = Http::asMultipart() - ->attach('c', $get('log')) - ->attach('e', '14d') - ->post('https://logs.pelican.dev'); - - if ($response->failed()) { - Notification::make() - ->title(trans('admin/node.diagnostics.upload_failed')) - ->body(fn () => $response->status() . ' - ' . $response->body()) - ->danger() - ->send(); - - return; - } - - $data = $response->json(); - $url = $data['url']; - - Notification::make() - ->title(trans('admin/node.diagnostics.logs_uploaded')) - ->body("{$url}") - ->success() - ->actions([ - Action::make('viewLogs') - ->label(trans('admin/node.diagnostics.view_logs')) - ->url($url) - ->openUrlInNewTab(true), - ]) - ->persistent() - ->send(); - $set('log', $url); - $set('pulled', false); - $set('uploaded', true); - - } catch (\Exception $e) { - Notification::make() - ->title(trans('admin/node.diagnostics.upload_failed')) - ->body($e->getMessage()) - ->danger() - ->send(); - } - }), - Action::make('clear') - ->label(trans('admin/node.diagnostics.clear')) - ->visible(fn (Get $get) => $get('pulled') ?? false) - ->icon('tabler-trash')->iconButton()->iconSize(IconSize::ExtraLarge)->color('danger') - ->action(function (Get $get, Set $set) { - $set('pulled', false); - $set('uploaded', false); - $set('log', null); - $this->refresh(); - } - ), - ]) - ->schema([ - ToggleButtons::make('include_endpoints') - ->hintIcon('tabler-question-mark')->inline() - ->hintIconTooltip(trans('admin/node.diagnostics.include_endpoints_hint')) - ->formatStateUsing(fn () => 1) - ->boolean(), - ToggleButtons::make('include_logs') - ->live() - ->hintIcon('tabler-question-mark')->inline() - ->hintIconTooltip(trans('admin/node.diagnostics.include_logs_hint')) - ->formatStateUsing(fn () => 1) - ->boolean(), - Slider::make('log_lines') - ->columnSpan(2) - ->hiddenLabel() - ->live() - ->tooltips(RawJs::make(<<<'JS' - `${$value} lines` - JS)) - ->visible(fn (Get $get) => $get('include_logs')) - ->range(minValue: 100, maxValue: 500) - ->pips(PipsMode::Steps, density: 10) - ->step(50) - ->formatStateUsing(fn () => 200) - ->fillTrack(), - Hidden::make('pulled'), - Hidden::make('uploaded'), - ]), - Textarea::make('log') - ->hiddenLabel() - ->columnSpanFull() - ->rows(35) - ->visible(fn (Get $get) => ($get('pulled') ?? false) || ($get('uploaded') ?? false)), - ]), - ]), - ]); - } -} From 8b6c3b0816dbaa422aa78c1f035daf0437c9a128 Mon Sep 17 00:00:00 2001 From: HarlequinSin Date: Fri, 2 Jan 2026 15:47:24 +1100 Subject: [PATCH 50/51] unused imports --- subdomains/src/Providers/SubdomainsPluginProvider.php | 1 - 1 file changed, 1 deletion(-) diff --git a/subdomains/src/Providers/SubdomainsPluginProvider.php b/subdomains/src/Providers/SubdomainsPluginProvider.php index d1cb0b7..948edb8 100644 --- a/subdomains/src/Providers/SubdomainsPluginProvider.php +++ b/subdomains/src/Providers/SubdomainsPluginProvider.php @@ -3,7 +3,6 @@ namespace Boy132\Subdomains\Providers; use App\Enums\HeaderActionPosition; -use App\Filament\Admin\Resources\Nodes\NodeResource; use App\Filament\Admin\Resources\Servers\ServerResource; use App\Models\Role; use App\Models\Server; From 2b30342d88c6e56499706c401fe9dcd1f34cf5a3 Mon Sep 17 00:00:00 2001 From: HarlequinSin Date: Fri, 2 Jan 2026 16:06:06 +1100 Subject: [PATCH 51/51] phpstan issues --- subdomains/src/Models/Subdomain.php | 12 ++++++------ subdomains/src/Services/CloudflareService.php | 4 ++-- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/subdomains/src/Models/Subdomain.php b/subdomains/src/Models/Subdomain.php index b24a4c2..3269537 100644 --- a/subdomains/src/Models/Subdomain.php +++ b/subdomains/src/Models/Subdomain.php @@ -115,12 +115,12 @@ public function getSrvRecordAttribute(): bool return $this->record_type === 'SRV'; } - public function setSrvRecordAttribute($isSrvRecord): void + public function setSrvRecordAttribute(bool $isSrvRecord): void { if ($isSrvRecord) { $this->attributes['record_type'] = 'SRV'; } else { - $ip = $this->server?->allocation?->ip ?? null; + $ip = $this->server?->allocation?->ip; if (!empty($ip) && filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) { $this->attributes['record_type'] = 'AAAA'; } else { @@ -133,7 +133,7 @@ protected function upsertOnCloudflare(): bool { $registrar = app(CloudflareService::class); - $zoneId = $this->domain?->cloudflare_id ?? null; + $zoneId = $this->domain?->cloudflare_id; if (empty($zoneId)) { Log::warning('Cloudflare zone id missing for domain', ['domain_id' => $this->domain_id]); @@ -148,7 +148,7 @@ protected function upsertOnCloudflare(): bool // SRV: target comes from node, port from server allocation if ($this->record_type === 'SRV') { - $port = $this->server->allocation?->port ?? null; + $port = $this->server->allocation?->port; if (empty($port)) { Log::warning('Server missing allocation with port', $this->toArray()); Notification::make() @@ -173,7 +173,7 @@ protected function upsertOnCloudflare(): bool } if (empty($this->server?->node?->srv_target)) { - Log::warning('Node missing SRV target for SRV record', ['node_id' => $this->server->node?->id ?? null]); + Log::warning('Node missing SRV target for SRV record', ['node_id' => $this->server->node?->id]); Notification::make() ->danger() ->title(trans('subdomains::strings.notifications.cloudflare_missing_srv_target_title')) @@ -205,7 +205,7 @@ protected function upsertOnCloudflare(): bool } // A/AAAA - $ip = $this->server?->allocation?->ip ?? null; + $ip = $this->server?->allocation?->ip; if (empty($ip) || $ip === '0.0.0.0' || $ip === '::') { Log::warning('Server allocation missing or invalid IP', ['server_id' => $this->server_id]); Notification::make() diff --git a/subdomains/src/Services/CloudflareService.php b/subdomains/src/Services/CloudflareService.php index af8cc9a..8e1d376 100644 --- a/subdomains/src/Services/CloudflareService.php +++ b/subdomains/src/Services/CloudflareService.php @@ -26,7 +26,7 @@ public function getZoneId(string $domainName): ?string return ['success' => false, 'errors' => ['exception' => $e->getMessage()], 'status' => $e->getCode(), 'body' => $e->getTraceAsString()]; } - $body = $response->json() ?? []; + $body = $response->json(); if ($response->successful() && !empty($body['result']) && count($body['result']) > 0) { return $body['result'][0]['id'] ?? null; @@ -146,7 +146,7 @@ public function deleteDnsRecord(string $zoneId, string $recordId): array protected function parseCloudflareHttpResponse(Response $response): array { $status = $response->status(); - $body = $response->json() ?? []; + $body = $response->json(); $success = $response->successful() && ($body['success'] === true || (is_array($body['result']) && count($body['result']) > 0));