From d61583cd7bc411daa84f64c3c54fb922e31107ce Mon Sep 17 00:00:00 2001 From: mristau Date: Tue, 4 Nov 2025 12:03:50 +0100 Subject: [PATCH 01/10] add server description to grid view too (#1851) --- .gitignore | 1 + resources/views/livewire/server-entry.blade.php | 11 ++++++++++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 85ae1a0e8..f82788f11 100644 --- a/.gitignore +++ b/.gitignore @@ -21,6 +21,7 @@ yarn-error.log /.idea /.nova /.vscode +/.ddev public/assets/manifest.json /database/*.sqlite* diff --git a/resources/views/livewire/server-entry.blade.php b/resources/views/livewire/server-entry.blade.php index c3ec13226..85c207bb1 100644 --- a/resources/views/livewire/server-entry.blade.php +++ b/resources/views/livewire/server-entry.blade.php @@ -22,7 +22,10 @@ "> @endif -
+
!$server->description, + ])> + @if ($server->description) +
+

{{ Str::limit($server->description, 40, preserveWords: true) }}

+
+ @endif +

{{ trans('server/dashboard.cpu') }}

From 852f7beb39df59d944d59b106f59c6245cf83b77 Mon Sep 17 00:00:00 2001 From: Boy132 Date: Tue, 4 Nov 2025 12:48:18 +0100 Subject: [PATCH 02/10] Allow to register "special file" alert banners (#1861) --- .../Resources/Files/Pages/EditFiles.php | 18 ++++++++----- app/Models/File.php | 27 +++++++++++++++++++ 2 files changed, 38 insertions(+), 7 deletions(-) diff --git a/app/Filament/Server/Resources/Files/Pages/EditFiles.php b/app/Filament/Server/Resources/Files/Pages/EditFiles.php index dd13e10b3..94c77a197 100644 --- a/app/Filament/Server/Resources/Files/Pages/EditFiles.php +++ b/app/Filament/Server/Resources/Files/Pages/EditFiles.php @@ -7,11 +7,13 @@ use App\Exceptions\Repository\FileNotEditableException; use App\Facades\Activity; use App\Filament\Server\Resources\Files\FileResource; use App\Livewire\AlertBanner; +use App\Models\File; use App\Models\Permission; use App\Models\Server; use App\Repositories\Daemon\DaemonFileRepository; use App\Traits\Filament\CanCustomizeHeaderActions; use App\Traits\Filament\CanCustomizeHeaderWidgets; +use Closure; use Filament\Actions\Action; use Filament\Facades\Filament; use Filament\Forms\Components\CodeEditor; @@ -215,13 +217,15 @@ class EditFiles extends Page $this->previousUrl = url()->previous(); - if (str($path)->endsWith('.pelicanignore')) { - AlertBanner::make('.pelicanignore_info') - ->title(trans('server/file.alerts.pelicanignore.title')) - ->body(trans('server/file.alerts.pelicanignore.body')) - ->info() - ->closable() - ->send(); + foreach (File::getSpecialFiles() as $fileName => $data) { + if ($data['check'] instanceof Closure && $data['check']($path)) { + AlertBanner::make($fileName . '_info') + ->title($data['title']) + ->body($data['body']) + ->info() + ->closable() + ->send(); + } } } diff --git a/app/Models/File.php b/app/Models/File.php index 47573f2d9..d04cff702 100644 --- a/app/Models/File.php +++ b/app/Models/File.php @@ -5,6 +5,7 @@ namespace App\Models; use App\Livewire\AlertBanner; use App\Repositories\Daemon\DaemonFileRepository; use Carbon\Carbon; +use Closure; use Exception; use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Model; @@ -54,6 +55,32 @@ class File extends Model protected static ?string $searchTerm; + /** @var array> */ + protected static array $customSpecialFiles = []; + + public static function registerSpecialFile(string $fileName, string|Closure $bannerTitle, string|Closure|null $bannerBody = null, ?Closure $nameCheck = null): void + { + static::$customSpecialFiles[$fileName] = [ + 'title' => $bannerTitle, + 'body' => $bannerBody, + 'check' => $nameCheck ?? fn (string $path) => str($path)->endsWith($fileName), + ]; + } + + /** @return array> */ + public static function getSpecialFiles(): array + { + $specialFiles = [ + '.pelicanignore' => [ + 'title' => fn () => trans('server/file.alerts.pelicanignore.title'), + 'body' => fn () => trans('server/file.alerts.pelicanignore.body'), + 'check' => fn (string $path) => str($path)->endsWith('.pelicanignore'), + ], + ]; + + return array_merge($specialFiles, static::$customSpecialFiles); + } + public static function get(Server $server, string $path = '/', ?string $searchTerm = null): Builder { self::$server = $server; From 4bda7cba75973c24cd6c7e6263144e9ef2299ce6 Mon Sep 17 00:00:00 2001 From: Boy132 Date: Wed, 5 Nov 2025 16:18:44 +0100 Subject: [PATCH 03/10] Allow to "embed" server list (#1860) --- .../App/Resources/Servers/ServerResource.php | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/app/Filament/App/Resources/Servers/ServerResource.php b/app/Filament/App/Resources/Servers/ServerResource.php index ba6f737d3..79b9b4ccb 100644 --- a/app/Filament/App/Resources/Servers/ServerResource.php +++ b/app/Filament/App/Resources/Servers/ServerResource.php @@ -10,10 +10,17 @@ class ServerResource extends Resource { protected static ?string $model = Server::class; + protected static string|\BackedEnum|null $navigationIcon = 'tabler-brand-docker'; + protected static ?string $slug = '/'; protected static bool $shouldRegisterNavigation = false; + public static function getNavigationBadge(): ?string + { + return (string) user()?->directAccessibleServers()->where('owner_id', user()?->id)->count(); + } + public static function canAccess(): bool { return true; @@ -25,4 +32,10 @@ class ServerResource extends Resource 'index' => ListServers::route('/'), ]; } + + public static function embedServerList(bool $condition = true): void + { + static::$slug = $condition ? null : '/'; + static::$shouldRegisterNavigation = $condition; + } } From e0c4e47a6cae63246f957aaea1894f468f33047e Mon Sep 17 00:00:00 2001 From: Boy132 Date: Wed, 5 Nov 2025 16:19:03 +0100 Subject: [PATCH 04/10] Fix `directAccessibleServers` returning duplicates (#1862) --- app/Models/User.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/Models/User.php b/app/Models/User.php index 2a115bdcc..32f71e45b 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -287,7 +287,8 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac ->leftJoin('subusers', 'subusers.server_id', '=', 'servers.id') ->where(function (Builder $builder) { $builder->where('servers.owner_id', $this->id)->orWhere('subusers.user_id', $this->id); - }); + }) + ->distinct('servers.id'); } public function accessibleNodes(): Builder From 49f24e37b6c32caf5b6f0a6f6789adf80bba753b Mon Sep 17 00:00:00 2001 From: Lance Pioch Date: Thu, 6 Nov 2025 08:43:02 -0500 Subject: [PATCH 05/10] Laravel 12.37.0 Shift (#1864) Co-authored-by: Shift --- composer.json | 2 +- composer.lock | 53 ++++++++++++++++++++++++++------------------------- 2 files changed, 28 insertions(+), 27 deletions(-) diff --git a/composer.json b/composer.json index 2ec143982..a428ffd19 100644 --- a/composer.json +++ b/composer.json @@ -15,7 +15,7 @@ "dedoc/scramble": "^0.12.10", "filament/filament": "~4.0", "guzzlehttp/guzzle": "^7.10", - "laravel/framework": "^12.31", + "laravel/framework": "^12.37", "laravel/helpers": "^1.7", "laravel/sanctum": "^4.2", "laravel/socialite": "^5.23", diff --git a/composer.lock b/composer.lock index 86d9a39f0..965d8c97d 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "101c2afb1f31acb872b4bed541397cd2", + "content-hash": "c8143eccd2736bd88b35d8fe6c8de289", "packages": [ { "name": "achyutn/filament-log-viewer", @@ -209,16 +209,16 @@ }, { "name": "aws/aws-sdk-php", - "version": "3.359.3", + "version": "3.359.4", "source": { "type": "git", "url": "https://github.com/aws/aws-sdk-php.git", - "reference": "a32e4c9522f0b61c947fafa1713d3a24b397a757" + "reference": "510cb4b7e2fa3ea09ad2154e7a13fe7675c36b30" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/a32e4c9522f0b61c947fafa1713d3a24b397a757", - "reference": "a32e4c9522f0b61c947fafa1713d3a24b397a757", + "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/510cb4b7e2fa3ea09ad2154e7a13fe7675c36b30", + "reference": "510cb4b7e2fa3ea09ad2154e7a13fe7675c36b30", "shasum": "" }, "require": { @@ -300,9 +300,9 @@ "support": { "forum": "https://github.com/aws/aws-sdk-php/discussions", "issues": "https://github.com/aws/aws-sdk-php/issues", - "source": "https://github.com/aws/aws-sdk-php/tree/3.359.3" + "source": "https://github.com/aws/aws-sdk-php/tree/3.359.4" }, - "time": "2025-10-31T18:15:22+00:00" + "time": "2025-11-03T19:18:23+00:00" }, { "name": "blade-ui-kit/blade-heroicons", @@ -2561,16 +2561,16 @@ }, { "name": "laravel/framework", - "version": "v12.36.1", + "version": "v12.37.0", "source": { "type": "git", "url": "https://github.com/laravel/framework.git", - "reference": "cad110d7685fbab990a6bb8184d0cfd847d7c4d8" + "reference": "3c3c4ad30f5b528b164a7c09aa4ad03118c4c125" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/framework/zipball/cad110d7685fbab990a6bb8184d0cfd847d7c4d8", - "reference": "cad110d7685fbab990a6bb8184d0cfd847d7c4d8", + "url": "https://api.github.com/repos/laravel/framework/zipball/3c3c4ad30f5b528b164a7c09aa4ad03118c4c125", + "reference": "3c3c4ad30f5b528b164a7c09aa4ad03118c4c125", "shasum": "" }, "require": { @@ -2776,7 +2776,7 @@ "issues": "https://github.com/laravel/framework/issues", "source": "https://github.com/laravel/framework" }, - "time": "2025-10-29T14:20:57+00:00" + "time": "2025-11-04T15:39:33+00:00" }, { "name": "laravel/helpers", @@ -7757,16 +7757,16 @@ }, { "name": "spatie/laravel-permission", - "version": "6.22.0", + "version": "6.23.0", "source": { "type": "git", "url": "https://github.com/spatie/laravel-permission.git", - "reference": "8c87966ddc21893bfda54b792047473703992625" + "reference": "9e41247bd512b1e6c229afbc1eb528f7565ae3bb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/spatie/laravel-permission/zipball/8c87966ddc21893bfda54b792047473703992625", - "reference": "8c87966ddc21893bfda54b792047473703992625", + "url": "https://api.github.com/repos/spatie/laravel-permission/zipball/9e41247bd512b1e6c229afbc1eb528f7565ae3bb", + "reference": "9e41247bd512b1e6c229afbc1eb528f7565ae3bb", "shasum": "" }, "require": { @@ -7828,7 +7828,7 @@ ], "support": { "issues": "https://github.com/spatie/laravel-permission/issues", - "source": "https://github.com/spatie/laravel-permission/tree/6.22.0" + "source": "https://github.com/spatie/laravel-permission/tree/6.23.0" }, "funding": [ { @@ -7836,7 +7836,7 @@ "type": "github" } ], - "time": "2025-10-27T21:58:45+00:00" + "time": "2025-11-03T20:16:13+00:00" }, { "name": "spatie/laravel-query-builder", @@ -12203,16 +12203,16 @@ }, { "name": "larastan/larastan", - "version": "v3.7.2", + "version": "v3.8.0", "source": { "type": "git", "url": "https://github.com/larastan/larastan.git", - "reference": "a761859a7487bd7d0cb8b662a7538a234d5bb5ae" + "reference": "d13ef96d652d1b2a8f34f1760ba6bf5b9c98112e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/larastan/larastan/zipball/a761859a7487bd7d0cb8b662a7538a234d5bb5ae", - "reference": "a761859a7487bd7d0cb8b662a7538a234d5bb5ae", + "url": "https://api.github.com/repos/larastan/larastan/zipball/d13ef96d652d1b2a8f34f1760ba6bf5b9c98112e", + "reference": "d13ef96d652d1b2a8f34f1760ba6bf5b9c98112e", "shasum": "" }, "require": { @@ -12226,7 +12226,7 @@ "illuminate/pipeline": "^11.44.2 || ^12.4.1", "illuminate/support": "^11.44.2 || ^12.4.1", "php": "^8.2", - "phpstan/phpstan": "^2.1.28" + "phpstan/phpstan": "^2.1.29" }, "require-dev": { "doctrine/coding-standard": "^13", @@ -12239,7 +12239,8 @@ "phpunit/phpunit": "^10.5.35 || ^11.5.15" }, "suggest": { - "orchestra/testbench": "Using Larastan for analysing a package needs Testbench" + "orchestra/testbench": "Using Larastan for analysing a package needs Testbench", + "phpmyadmin/sql-parser": "Install to enable Larastan's optional phpMyAdmin-based SQL parser automatically" }, "type": "phpstan-extension", "extra": { @@ -12280,7 +12281,7 @@ ], "support": { "issues": "https://github.com/larastan/larastan/issues", - "source": "https://github.com/larastan/larastan/tree/v3.7.2" + "source": "https://github.com/larastan/larastan/tree/v3.8.0" }, "funding": [ { @@ -12288,7 +12289,7 @@ "type": "github" } ], - "time": "2025-09-19T09:03:05+00:00" + "time": "2025-10-27T23:09:14+00:00" }, { "name": "laravel/pail", From 6ed84b5584fda8ae1f4c96a3fcedf9795051eeaf Mon Sep 17 00:00:00 2001 From: Charles Date: Sat, 8 Nov 2025 15:47:40 -0500 Subject: [PATCH 06/10] Add wings `diagnostics` retrieving to Edit Node page (#1865) Co-authored-by: Boy132 --- .../Admin/Resources/Nodes/Pages/EditNode.php | 164 +++++++++++++++++- app/Models/Node.php | 4 +- ...ository.php => DaemonSystemRepository.php} | 19 +- app/Services/Nodes/NodeUpdateService.php | 4 +- lang/en/admin/node.php | 19 ++ 5 files changed, 200 insertions(+), 10 deletions(-) rename app/Repositories/Daemon/{DaemonConfigurationRepository.php => DaemonSystemRepository.php} (69%) diff --git a/app/Filament/Admin/Resources/Nodes/Pages/EditNode.php b/app/Filament/Admin/Resources/Nodes/Pages/EditNode.php index 175e4a1c2..29aedeb01 100644 --- a/app/Filament/Admin/Resources/Nodes/Pages/EditNode.php +++ b/app/Filament/Admin/Resources/Nodes/Pages/EditNode.php @@ -4,7 +4,7 @@ namespace App\Filament\Admin\Resources\Nodes\Pages; use App\Filament\Admin\Resources\Nodes\NodeResource; use App\Models\Node; -use App\Repositories\Daemon\DaemonConfigurationRepository; +use App\Repositories\Daemon\DaemonSystemRepository; use App\Services\Helpers\SoftwareVersionService; use App\Services\Nodes\NodeAutoDeployService; use App\Services\Nodes\NodeUpdateService; @@ -14,6 +14,8 @@ 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; use Filament\Forms\Components\TagsInput; use Filament\Forms\Components\Textarea; use Filament\Forms\Components\TextInput; @@ -25,6 +27,7 @@ use Filament\Resources\Pages\EditRecord; use Filament\Schemas\Components\Actions; use Filament\Schemas\Components\Fieldset; use Filament\Schemas\Components\Grid; +use Filament\Schemas\Components\Section; use Filament\Schemas\Components\StateCasts\BooleanStateCast; use Filament\Schemas\Components\Tabs; use Filament\Schemas\Components\Tabs\Tab; @@ -33,7 +36,10 @@ use Filament\Schemas\Components\Utilities\Set; use Filament\Schemas\Components\View; use Filament\Schemas\Schema; use Filament\Support\Enums\Alignment; +use Filament\Support\Enums\IconSize; +use Filament\Support\RawJs; use Illuminate\Http\Client\ConnectionException; +use Illuminate\Support\Facades\Http; use Illuminate\Support\HtmlString; use Phiki\Grammar\Grammar; use Throwable; @@ -45,13 +51,13 @@ class EditNode extends EditRecord protected static string $resource = NodeResource::class; - private DaemonConfigurationRepository $daemonConfigurationRepository; + private DaemonSystemRepository $daemonSystemRepository; private NodeUpdateService $nodeUpdateService; - public function boot(DaemonConfigurationRepository $daemonConfigurationRepository, NodeUpdateService $nodeUpdateService): void + public function boot(DaemonSystemRepository $daemonSystemRepository, NodeUpdateService $nodeUpdateService): void { - $this->daemonConfigurationRepository = $daemonConfigurationRepository; + $this->daemonSystemRepository = $daemonSystemRepository; $this->nodeUpdateService = $nodeUpdateService; } @@ -624,6 +630,154 @@ class EditNode extends EditRecord ])->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()->post('https://logs.pelican.dev', [ + [ + 'name' => 'c', + 'contents' => $get('log'), + ], + [ + 'name' => 'e', + 'contents' => '14d', + ], + ]); + + 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)), + ]), ]), ]); } @@ -681,7 +835,7 @@ class EditNode extends EditRecord try { if ($changed) { - $this->daemonConfigurationRepository->setNode($node)->update($node); + $this->daemonSystemRepository->setNode($node)->update($node); } parent::getSavedNotification()?->send(); } catch (ConnectionException) { diff --git a/app/Models/Node.php b/app/Models/Node.php index 52b02ac11..e4f127d84 100644 --- a/app/Models/Node.php +++ b/app/Models/Node.php @@ -4,7 +4,7 @@ namespace App\Models; use App\Contracts\Validatable; use App\Exceptions\Service\HasActiveServersException; -use App\Repositories\Daemon\DaemonConfigurationRepository; +use App\Repositories\Daemon\DaemonSystemRepository; use App\Traits\HasValidation; use Carbon\Carbon; use Exception; @@ -316,7 +316,7 @@ class Node extends Model implements Validatable { return once(function () { try { - return (new DaemonConfigurationRepository()) + return (new DaemonSystemRepository()) ->setNode($this) ->getSystemInformation(); } catch (Exception $exception) { diff --git a/app/Repositories/Daemon/DaemonConfigurationRepository.php b/app/Repositories/Daemon/DaemonSystemRepository.php similarity index 69% rename from app/Repositories/Daemon/DaemonConfigurationRepository.php rename to app/Repositories/Daemon/DaemonSystemRepository.php index 916e7d459..71d88a138 100644 --- a/app/Repositories/Daemon/DaemonConfigurationRepository.php +++ b/app/Repositories/Daemon/DaemonSystemRepository.php @@ -6,7 +6,7 @@ use App\Models\Node; use Illuminate\Http\Client\ConnectionException; use Illuminate\Http\Client\Response; -class DaemonConfigurationRepository extends DaemonRepository +class DaemonSystemRepository extends DaemonRepository { /** * Returns system information from the daemon instance. @@ -30,6 +30,23 @@ class DaemonConfigurationRepository extends DaemonRepository })->json(); } + /** + * Retrieve diagnostics from the daemon for the current node. + * + * + * @throws ConnectionException + */ + public function getDiagnostics(int $lines, bool $includeEndpoints, bool $includeLogs): Response + { + return $this->getHttpClient() + ->timeout(5) + ->get('/api/diagnostics', [ + 'log_lines' => $lines, + 'include_endpoints' => $includeEndpoints ? 'true' : 'false', + 'include_logs' => $includeLogs ? 'true' : 'false', + ]); + } + /** * Updates the configuration information for a daemon. Updates the information for * this instance using a passed-in model. This allows us to change plenty of information diff --git a/app/Services/Nodes/NodeUpdateService.php b/app/Services/Nodes/NodeUpdateService.php index 13b094979..6ef132a46 100644 --- a/app/Services/Nodes/NodeUpdateService.php +++ b/app/Services/Nodes/NodeUpdateService.php @@ -4,7 +4,7 @@ namespace App\Services\Nodes; use App\Exceptions\Service\Node\ConfigurationNotPersistedException; use App\Models\Node; -use App\Repositories\Daemon\DaemonConfigurationRepository; +use App\Repositories\Daemon\DaemonSystemRepository; use Illuminate\Database\ConnectionInterface; use Illuminate\Http\Client\ConnectionException; use Illuminate\Support\Str; @@ -17,7 +17,7 @@ class NodeUpdateService */ public function __construct( private ConnectionInterface $connection, - private DaemonConfigurationRepository $configurationRepository, + private DaemonSystemRepository $configurationRepository, ) {} /** diff --git a/lang/en/admin/node.php b/lang/en/admin/node.php index a726300ee..961ca5169 100644 --- a/lang/en/admin/node.php +++ b/lang/en/admin/node.php @@ -10,6 +10,7 @@ return [ 'basic_settings' => 'Basic Settings', 'advanced_settings' => 'Advanced Settings', 'config_file' => 'Configuration File', + 'diagnostics' => 'Diagnostics', ], 'table' => [ 'health' => 'Health', @@ -117,6 +118,24 @@ return [ 'error_connecting_description' => 'The configuration could not be automatically updated on Wings, you will need to manually update the configuration file.', 'allocation' => 'Allocation', + 'diagnostics' => [ + 'header' => 'Node Diagnostics', + 'include_endpoints' => 'Include Endpoints', + 'include_endpoints_hint' => 'Including endpoints will show panel urls within the logs and NOT obscure them.', + 'include_logs' => 'Include Logs', + 'include_logs_hint' => 'Including logs will show recent logs and help track down possible issues.', + 'run_diagnostics' => 'Run Diagnostics', + 'upload_to_pelican' => 'Upload Logs', + 'logs_pulled' => 'Logs Pulled!', + 'logs_uploaded' => 'Logs Uploaded', + 'upload_failed' => 'Logs Upload Failed', + 'view_logs' => 'View Logs', + 'pull' => 'Pull', + 'upload' => 'Upload', + 'clear' => 'Clear', + '404' => 'The requested diagnostic report could not be found. Make sure wings is up to date and try again.', + ], + 'cloudflare_issue' => [ 'title' => 'Cloudflare Issue', 'body' => 'Your Node is not accessible by Cloudflare', From cec141889a0e77bf2e732ff12a17198ec6d04cb8 Mon Sep 17 00:00:00 2001 From: Boy132 Date: Sat, 8 Nov 2025 21:54:41 +0100 Subject: [PATCH 07/10] Allow admins to "lock" allocations (#1811) --- .../AllocationsRelationManager.php | 41 ++++++++++++++++--- .../Allocations/AllocationResource.php | 11 ++++- app/Models/Allocation.php | 13 ++++++ .../Servers/ServerCreationService.php | 1 + ...14_065517_add_is_locked_to_allocations.php | 28 +++++++++++++ lang/en/admin/server.php | 4 ++ lang/en/server/network.php | 2 + 7 files changed, 93 insertions(+), 7 deletions(-) create mode 100644 database/migrations/2025_10_14_065517_add_is_locked_to_allocations.php diff --git a/app/Filament/Admin/Resources/Servers/RelationManagers/AllocationsRelationManager.php b/app/Filament/Admin/Resources/Servers/RelationManagers/AllocationsRelationManager.php index 296a0fdb8..ad6d21208 100644 --- a/app/Filament/Admin/Resources/Servers/RelationManagers/AllocationsRelationManager.php +++ b/app/Filament/Admin/Resources/Servers/RelationManagers/AllocationsRelationManager.php @@ -60,16 +60,35 @@ class AllocationsRelationManager extends RelationManager ->action(fn (Allocation $allocation) => $this->getOwnerRecord()->update(['allocation_id' => $allocation->id]) && $this->deselectAllTableRecords()) ->default(fn (Allocation $allocation) => $allocation->id === $this->getOwnerRecord()->allocation_id) ->label(trans('admin/server.primary')), + IconColumn::make('is_locked') + ->label(trans('admin/server.locked')) + ->tooltip(trans('admin/server.locked_helper')) + ->trueIcon('tabler-lock') + ->falseIcon('tabler-lock-open'), ]) ->recordActions([ Action::make('make-primary') ->label(trans('admin/server.make_primary')) ->action(fn (Allocation $allocation) => $this->getOwnerRecord()->update(['allocation_id' => $allocation->id]) && $this->deselectAllTableRecords()) ->hidden(fn (Allocation $allocation) => $allocation->id === $this->getOwnerRecord()->allocation_id), + Action::make('lock') + ->label(trans('admin/server.lock')) + ->action(fn (Allocation $allocation) => $allocation->update(['is_locked' => true]) && $this->deselectAllTableRecords()) + ->hidden(fn (Allocation $allocation) => $allocation->is_locked), + Action::make('unlock') + ->label(trans('admin/server.unlock')) + ->action(fn (Allocation $allocation) => $allocation->update(['is_locked' => false]) && $this->deselectAllTableRecords()) + ->visible(fn (Allocation $allocation) => $allocation->is_locked), DissociateAction::make() ->after(function (Allocation $allocation) { - $allocation->update(['notes' => null]); - $this->getOwnerRecord()->allocation_id && $this->getOwnerRecord()->update(['allocation_id' => $this->getOwnerRecord()->allocations()->first()?->id]); + $allocation->update([ + 'notes' => null, + 'is_locked' => false, + ]); + + if (!$this->getOwnerRecord()->allocation_id) { + $this->getOwnerRecord()->update(['allocation_id' => $this->getOwnerRecord()->allocations()->first()?->id]); + } }), ]) ->headerActions([ @@ -116,13 +135,25 @@ class AllocationsRelationManager extends RelationManager ->recordSelectOptionsQuery(fn ($query) => $query->whereBelongsTo($this->getOwnerRecord()->node)->whereNull('server_id')) ->recordSelectSearchColumns(['ip', 'port']) ->label(trans('admin/server.add_allocation')) - ->after(fn (array $data) => !$this->getOwnerRecord()->allocation_id && $this->getOwnerRecord()->update(['allocation_id' => $data['recordId'][0]])), + ->after(function (array $data) { + Allocation::whereIn('id', array_values(array_unique($data['recordId'])))->update(['is_locked' => true]); + + if (!$this->getOwnerRecord()->allocation_id) { + $this->getOwnerRecord()->update(['allocation_id' => $data['recordId'][0]]); + } + }), ]) ->groupedBulkActions([ DissociateBulkAction::make() ->after(function () { - Allocation::whereNull('server_id')->update(['notes' => null]); - $this->getOwnerRecord()->allocation_id && $this->getOwnerRecord()->update(['allocation_id' => $this->getOwnerRecord()->allocations()->first()?->id]); + Allocation::whereNull('server_id')->update([ + 'notes' => null, + 'is_locked' => false, + ]); + + if (!$this->getOwnerRecord()->allocation_id) { + $this->getOwnerRecord()->update(['allocation_id' => $this->getOwnerRecord()->allocations()->first()?->id]); + } }), ]); } diff --git a/app/Filament/Server/Resources/Allocations/AllocationResource.php b/app/Filament/Server/Resources/Allocations/AllocationResource.php index 92e70c554..2cfad97f8 100644 --- a/app/Filament/Server/Resources/Allocations/AllocationResource.php +++ b/app/Filament/Server/Resources/Allocations/AllocationResource.php @@ -73,15 +73,22 @@ class AllocationResource extends Resource ->action(fn (Allocation $allocation) => user()?->can(PERMISSION::ACTION_ALLOCATION_UPDATE, $server) && $server->update(['allocation_id' => $allocation->id])) ->default(fn (Allocation $allocation) => $allocation->id === $server->allocation_id) ->label(trans('server/network.primary')), + IconColumn::make('is_locked') + ->label(trans('server/network.locked')) + ->tooltip(trans('server/network.locked_helper')) + ->trueIcon('tabler-lock') + ->falseIcon('tabler-lock-open'), ]) ->recordActions([ DetachAction::make() + ->visible(fn (Allocation $allocation) => !$allocation->is_locked || user()?->can('update', $allocation->node)) ->authorize(fn () => user()?->can(Permission::ACTION_ALLOCATION_DELETE, $server)) ->label(trans('server/network.delete')) ->icon('tabler-trash') ->action(function (Allocation $allocation) { - Allocation::query()->where('id', $allocation->id)->update([ + Allocation::where('id', $allocation->id)->update([ 'notes' => null, + 'is_locked' => false, 'server_id' => null, ]); @@ -93,7 +100,7 @@ class AllocationResource extends Resource ->after(fn (Allocation $allocation) => $allocation->id === $server->allocation_id && $server->update(['allocation_id' => $server->allocations()->first()?->id])), ]) ->toolbarActions([ - Action::make('addAllocation') + Action::make('add_allocation') ->hiddenLabel()->iconButton()->iconSize(IconSize::ExtraLarge) ->icon(fn () => $server->allocations()->count() >= $server->allocation_limit ? 'tabler-network-off' : 'tabler-network') ->authorize(fn () => user()?->can(Permission::ACTION_ALLOCATION_CREATE, $server)) diff --git a/app/Models/Allocation.php b/app/Models/Allocation.php index 6c7ac88a9..fc529829c 100644 --- a/app/Models/Allocation.php +++ b/app/Models/Allocation.php @@ -29,6 +29,7 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo; * @property string $address * @property Server|null $server * @property Node $node + * @property bool $is_locked * * @method static AllocationFactory factory(...$parameters) * @method static Builder|Allocation newModelQuery() @@ -55,6 +56,10 @@ class Allocation extends Model */ public const RESOURCE_NAME = 'allocation'; + protected $attributes = [ + 'is_locked' => false, + ]; + /** * Fields that are not mass assignable. */ @@ -68,10 +73,17 @@ class Allocation extends Model 'ip_alias' => ['nullable', 'string'], 'server_id' => ['nullable', 'exists:servers,id'], 'notes' => ['nullable', 'string', 'max:256'], + 'is_locked' => ['boolean'], ]; protected static function booted(): void { + static::updating(function (self $allocation) { + if (is_null($allocation->server_id)) { + $allocation->is_locked = false; + } + }); + static::deleting(function (self $allocation) { throw_if($allocation->server_id, new ServerUsingAllocationException(trans('exceptions.allocations.server_using'))); }); @@ -83,6 +95,7 @@ class Allocation extends Model 'node_id' => 'integer', 'port' => 'integer', 'server_id' => 'integer', + 'is_locked' => 'bool', ]; } diff --git a/app/Services/Servers/ServerCreationService.php b/app/Services/Servers/ServerCreationService.php index f2483c5f6..f15f84f34 100644 --- a/app/Services/Servers/ServerCreationService.php +++ b/app/Services/Servers/ServerCreationService.php @@ -191,6 +191,7 @@ class ServerCreationService ->get() ->each(function (Allocation $allocation) use ($server) { $allocation->server_id = $server->id; + $allocation->is_locked = true; $allocation->save(); }); } diff --git a/database/migrations/2025_10_14_065517_add_is_locked_to_allocations.php b/database/migrations/2025_10_14_065517_add_is_locked_to_allocations.php new file mode 100644 index 000000000..f7c429cb2 --- /dev/null +++ b/database/migrations/2025_10_14_065517_add_is_locked_to_allocations.php @@ -0,0 +1,28 @@ +boolean('is_locked')->default(false); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('allocations', function (Blueprint $table) { + $table->dropColumn('is_locked'); + }); + } +}; diff --git a/lang/en/admin/server.php b/lang/en/admin/server.php index cd74c4159..935ca8232 100644 --- a/lang/en/admin/server.php +++ b/lang/en/admin/server.php @@ -13,6 +13,10 @@ return [ 'ports' => 'Ports', 'alias' => 'Alias', 'alias_helper' => 'Optional display name to help you remember what these are.', + 'locked' => 'Locked?', + 'locked_helper' => 'Users won\'t be able to delete locked allocations', + 'lock' => 'Lock', + 'unlock' => 'Unlock', 'name' => 'Name', 'external_id' => 'External ID', 'owner' => 'Owner', diff --git a/lang/en/server/network.php b/lang/en/server/network.php index 21ef09012..6ff0bf1bb 100644 --- a/lang/en/server/network.php +++ b/lang/en/server/network.php @@ -12,4 +12,6 @@ return [ 'primary' => 'Primary', 'make' => 'Make', 'delete' => 'Delete', + 'locked' => 'Locked?', + 'locked_helper' => 'Locked allocations can only be deleted by admins', ]; From 1ff965611e32dd88a21533bf54a62af98feab038 Mon Sep 17 00:00:00 2001 From: exefer Date: Sat, 8 Nov 2025 22:40:23 +0100 Subject: [PATCH 08/10] Fix typo in DNS help text (#1868) Authored-by: RMartinOscar <40749467+RMartinOscar@users.noreply.github.com> --- lang/en/admin/node.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lang/en/admin/node.php b/lang/en/admin/node.php index 961ca5169..745c63afb 100644 --- a/lang/en/admin/node.php +++ b/lang/en/admin/node.php @@ -44,7 +44,7 @@ return [ 'error' => 'This is the domain name that points to your node\'s IP Address. If you\'ve already set up this, you can verify it by checking the next field!', 'fqdn_help' => 'Your panel is currently secured via an SSL certificate and that means your nodes require one too. You must use a domain name, because you cannot get SSL certificates for IP Addresses.', 'dns' => 'DNS Record Check', - 'dns_help' => 'This lets you know if you DNS record is pointing to the correct IP address.', + 'dns_help' => 'This lets you know if your DNS record is pointing to the correct IP address.', 'valid' => 'Valid', 'invalid' => 'Invalid', 'port' => 'Port', From b06df238237bbceaa6774a23e3d54f321276c822 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Sat, 8 Nov 2025 16:53:12 -0500 Subject: [PATCH 09/10] Add bulk IP update action for node allocations (#1845) Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: notAreYouScared <1757840+notAreYouScared@users.noreply.github.com> Co-authored-by: Charles --- .../AllocationsRelationManager.php | 10 +- .../Actions/UpdateNodeAllocations.php | 106 ++++++++++++++++++ lang/en/admin/node.php | 9 ++ 3 files changed, 121 insertions(+), 4 deletions(-) create mode 100644 app/Filament/Components/Actions/UpdateNodeAllocations.php diff --git a/app/Filament/Admin/Resources/Nodes/RelationManagers/AllocationsRelationManager.php b/app/Filament/Admin/Resources/Nodes/RelationManagers/AllocationsRelationManager.php index 3846c562f..b421e6d83 100644 --- a/app/Filament/Admin/Resources/Nodes/RelationManagers/AllocationsRelationManager.php +++ b/app/Filament/Admin/Resources/Nodes/RelationManagers/AllocationsRelationManager.php @@ -3,6 +3,7 @@ namespace App\Filament\Admin\Resources\Nodes\RelationManagers; use App\Filament\Admin\Resources\Servers\Pages\CreateServer; +use App\Filament\Components\Actions\UpdateNodeAllocations; use App\Models\Allocation; use App\Models\Node; use App\Services\Allocations\AssignmentService; @@ -80,7 +81,9 @@ class AllocationsRelationManager extends RelationManager ->searchable() ->label(trans('admin/node.table.ip')), ]) - ->headerActions([ + ->toolbarActions([ + DeleteBulkAction::make() + ->authorize(fn () => user()?->can('update', $this->getOwnerRecord())), Action::make('create new allocation') ->label(trans('admin/node.create_allocation')) ->schema(fn () => [ @@ -118,9 +121,8 @@ class AllocationsRelationManager extends RelationManager ->required(), ]) ->action(fn (array $data, AssignmentService $service) => $service->handle($this->getOwnerRecord(), $data)), - ]) - ->groupedBulkActions([ - DeleteBulkAction::make() + UpdateNodeAllocations::make() + ->nodeRecord($this->getOwnerRecord()) ->authorize(fn () => user()?->can('update', $this->getOwnerRecord())), ]); } diff --git a/app/Filament/Components/Actions/UpdateNodeAllocations.php b/app/Filament/Components/Actions/UpdateNodeAllocations.php new file mode 100644 index 000000000..cf186f82c --- /dev/null +++ b/app/Filament/Components/Actions/UpdateNodeAllocations.php @@ -0,0 +1,106 @@ +label(trans('admin/node.bulk_update_ip')); + + $this->icon('tabler-replace'); + + $this->color('warning'); + + $this->requiresConfirmation(); + + $this->modalHeading(trans('admin/node.bulk_update_ip')); + + $this->modalDescription(trans('admin/node.bulk_update_ip_description')); + + $this->modalIconColor('warning'); + + $this->modalSubmitActionLabel(trans('admin/node.update_ip')); + + $this->schema(function () { + /** @var Node $node */ + $node = $this->record; + + $currentIps = Allocation::where('node_id', $node->id) + ->pluck('ip') + ->unique() + ->values() + ->all(); + + return [ + Select::make('old_ip') + ->label(trans('admin/node.old_ip')) + ->options(array_combine($currentIps, $currentIps)) + ->selectablePlaceholder(false) + ->required() + ->live(), + Select::make('new_ip') + ->label(trans('admin/node.new_ip')) + ->options(fn () => array_combine($node->ipAddresses(), $node->ipAddresses()) ?: []) + ->required() + ->different('old_ip'), + ]; + }); + + $this->action(function (array $data) { + /** @var Node $node */ + $node = $this->record; + $allocations = Allocation::where('node_id', $node->id)->where('ip', $data['old_ip'])->get(); + + if ($allocations->count() === 0) { + Notification::make() + ->title(trans('admin/node.no_allocations_to_update')) + ->warning() + ->send(); + + return; + } + + $updated = 0; + $failed = 0; + + foreach ($allocations as $allocation) { + try { + $allocation->update(['ip' => $data['new_ip']]); + $updated++; + } catch (Exception $exception) { + $failed++; + report($exception); + } + } + + Notification::make() + ->title(trans('admin/node.ip_updated', ['count' => $updated, 'total' => $allocations->count()])) + ->body($failed > 0 ? trans('admin/node.ip_update_failed', ['count' => $failed]) : null) + ->status($failed > 0 ? 'warning' : 'success') + ->persistent() + ->send(); + }); + } + + public function nodeRecord(Node $node): static + { + $this->record = $node; + + return $this; + } +} diff --git a/lang/en/admin/node.php b/lang/en/admin/node.php index 745c63afb..40b514673 100644 --- a/lang/en/admin/node.php +++ b/lang/en/admin/node.php @@ -140,4 +140,13 @@ return [ 'title' => 'Cloudflare Issue', 'body' => 'Your Node is not accessible by Cloudflare', ], + + 'bulk_update_ip' => 'Update IPs', + 'bulk_update_ip_description' => 'Replace an old IP address with a new one for allocations. This is useful when a node\'s IP address changes', + 'update_ip' => 'Update IP', + 'old_ip' => 'Old IP Address', + 'new_ip' => 'New IP Address', + 'no_allocations_to_update' => 'No allocations with the selected old IP address were found', + 'ip_updated' => 'Successfully updated :count of :total allocation(s)', + 'ip_update_failed' => ':count allocation(s) failed to update', ]; From a30c45fbbecb44b13d5a3dd2bd01d7cf9766ceed Mon Sep 17 00:00:00 2001 From: Charles Date: Sat, 8 Nov 2025 17:09:41 -0500 Subject: [PATCH 10/10] Add session key to use last used node, instead of latest created node (#1869) Co-authored-by: Lance Pioch --- .../Admin/Resources/Servers/Pages/CreateServer.php | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/app/Filament/Admin/Resources/Servers/Pages/CreateServer.php b/app/Filament/Admin/Resources/Servers/Pages/CreateServer.php index 2c44cef7b..7db8e8fc1 100644 --- a/app/Filament/Admin/Resources/Servers/Pages/CreateServer.php +++ b/app/Filament/Admin/Resources/Servers/Pages/CreateServer.php @@ -116,6 +116,14 @@ class CreateServer extends CreateRecord ->prefixIcon('tabler-server-2') ->selectablePlaceholder(false) ->default(function () { + $lastUsedNode = session()->get('last_utilized_node'); + + if ($lastUsedNode && user()?->accessibleNodes()->where('id', $lastUsedNode)->exists()) { + $this->node = Node::find($lastUsedNode); + + return $this->node?->id; + } + /** @var ?Node $latestNode */ $latestNode = user()?->accessibleNodes()->latest()->first(); $this->node = $latestNode; @@ -829,6 +837,8 @@ class CreateServer extends CreateRecord $data['allocation_additional'] = collect($allocation_additional)->filter()->all(); } + session()->put('last_utilized_node', $data['node_id']); + try { return $this->serverCreationService->handle($data); } catch (Exception $exception) {