diff --git a/app/BlueprintFramework/Libraries/ExtensionLibrary/BlueprintBaseLibrary.php b/app/BlueprintFramework/Libraries/ExtensionLibrary/BlueprintBaseLibrary.php index 2d3574c4..f817d1ef 100644 --- a/app/BlueprintFramework/Libraries/ExtensionLibrary/BlueprintBaseLibrary.php +++ b/app/BlueprintFramework/Libraries/ExtensionLibrary/BlueprintBaseLibrary.php @@ -14,9 +14,10 @@ namespace Pterodactyl\BlueprintFramework\Libraries\ExtensionLibrary; -use Illuminate\Support\Facades\DB; -use Illuminate\Support\Collection; use Symfony\Component\Yaml\Yaml; +use Illuminate\Support\Collection; +use Illuminate\Support\Facades\DB; +use Pterodactyl\Models\ExtensionCachedMetadata; class BlueprintBaseLibrary { @@ -294,6 +295,39 @@ public function extensionConfig(string $identifier): ?array return $conf; } + /** + * Retrieves the stored metadata for an extension. + * + * This method checks if the given extension has available metadata and, if so, + * returns the metadata. Please note that this metadata may be delayed, and the + * format of which may change, so parse wisely. + * + * Requires the 'remote_metadata' flag to be set to true in Blueprint's settings. + * + * @param string $identifier Extension identifier to retrieve metadata for + * + * @return array|null The metadata array for the extension, or null if not available. + */ + public function extensionMetadata(string $identifier): ?array + { + $metadata = []; + if (!$this->extension($identifier)) { + return null; + } + if($this->dbGet('blueprint', 'flags:remote_metadata')) { + $metadata = ExtensionCachedMetadata::whereIn('identifier', $this->extensions()) + ->get() + ->keyBy('identifier') + ->map(fn($m) => $m->metadata) + ->toArray(); + + if(isset($metadata[$identifier])) { + return $metadata[$identifier]; + } + } + return null; + } + /** * Returns a Collection containing all installed extensions's configs. * diff --git a/app/Console/Commands/BlueprintFramework/MetadataCacheCommand.php b/app/Console/Commands/BlueprintFramework/MetadataCacheCommand.php new file mode 100644 index 00000000..b2ee1bb4 --- /dev/null +++ b/app/Console/Commands/BlueprintFramework/MetadataCacheCommand.php @@ -0,0 +1,89 @@ +blueprint->dbGet("blueprint", "flags:remote_metadata")) { + $this->error('remote_metadata flag set to false'); + return false; + } + + $now = now(); + $rows = []; + $installedExtensions = $this->blueprint->extensions(); + + // get version info + $context = stream_context_create(['http' => ['method' => 'GET', 'header' => 'User-Agent: BlueprintFramework']]); + $remoteVersions = @file_get_contents( + $this->PlaceholderService->api_url() . '/api/extensions/latest', + false, + $context + ); + + if($remoteVersions) { + $remoteVersionsData = json_decode($remoteVersions, true); + } + + if(! isset($remoteVersionsData)) { + $this->error('failed to fetch extension versions'); + return false; + } + + foreach ($installedExtensions as $identifier) { + if(! isset($remoteVersionsData[$identifier]) || ! is_scalar($remoteVersionsData[$identifier])) continue; + + $local_extension = $this->blueprint->extensionConfig($identifier); + + $rows[] = [ + 'identifier' => $identifier, + 'metadata' => json_encode([ + 'latest_version' => (string) $remoteVersionsData[$identifier], + 'local_version' => (string) $local_extension['info']['version'] ?? '', + ]), + 'fetched_at' => $now, + ]; + } + + if (empty($rows)) { + $this->info('no relevant data available, do you have any extensions installed?'); + return false; + } + + $table = (new ExtensionCachedMetadata())->getTable(); + + DB::transaction(function () use ($rows, $table) { + DB::table($table)->delete(); + DB::table($table)->insert($rows); + }); + + $this->info('updated extension cached metadata'); + } +} diff --git a/app/Console/Kernel.php b/app/Console/Kernel.php index 34ff43a5..fad9a76e 100644 --- a/app/Console/Kernel.php +++ b/app/Console/Kernel.php @@ -54,6 +54,10 @@ protected function schedule(Schedule $schedule): void $this->registerTelemetry($schedule); } + // ============================ + // BLUEPRINT SCHEDULES + // ============================ + // Blueprint telemetry $blueprint = app()->make(BlueprintExtensionLibrary::class); if ($blueprint->dbGet('blueprint', 'flags:telemetry_enabled', 0)) { @@ -62,7 +66,9 @@ protected function schedule(Schedule $schedule): void } // Blueprint-related utilities - $schedule->command('bp:version:cache')->dailyAt(str_pad(rand(0, 23), 2, '0', STR_PAD_LEFT) . ':' . str_pad(rand(0, 59), 2, '0', STR_PAD_LEFT)); + $randTime = str_pad(rand(0, 23), 2, '0', STR_PAD_LEFT) . ':' . str_pad(rand(0, 59), 2, '0', STR_PAD_LEFT); + $schedule->command('bp:version:cache')->dailyAt($randTime); + $schedule->command('bp:meta')->dailyAt($randTime); // Blueprint extension schedules GetExtensionSchedules::schedules($schedule); diff --git a/app/Http/Controllers/Admin/Extensions/Blueprint/BlueprintExtensionController.php b/app/Http/Controllers/Admin/Extensions/Blueprint/BlueprintExtensionController.php index e8b48044..0995ea6f 100644 --- a/app/Http/Controllers/Admin/Extensions/Blueprint/BlueprintExtensionController.php +++ b/app/Http/Controllers/Admin/Extensions/Blueprint/BlueprintExtensionController.php @@ -2,13 +2,13 @@ namespace Pterodactyl\Http\Controllers\Admin\Extensions\Blueprint; -use Pterodactyl\Http\Controllers\Controller; -use Pterodactyl\Contracts\Repository\SettingsRepositoryInterface; use Illuminate\Http\RedirectResponse; -use Pterodactyl\Http\Requests\Admin\AdminFormRequest; use Database\Seeders\BlueprintSeeder; +use Illuminate\Support\Facades\Artisan; +use Pterodactyl\Http\Controllers\Controller; +use Pterodactyl\Http\Requests\Admin\AdminFormRequest; +use Pterodactyl\Contracts\Repository\SettingsRepositoryInterface; -// FIXME: Move form request and remove controller. class BlueprintExtensionController extends Controller { @@ -26,10 +26,17 @@ public function __construct( */ public function update(BlueprintAdminFormRequest $request): RedirectResponse { + $meta_flag = $this->settings->get('blueprint::flags:remote_metadata'); + foreach ($request->validated() as $key => $value) { $this->settings->set('blueprint::' . $key, $value); } + // refresh meta if the flag has been altered + if($meta_flag != $request->validated()['flags:remote_metadata']) { + Artisan::call('bp:meta'); + } + return redirect()->route('admin.extensions'); } } @@ -41,11 +48,11 @@ public function rules(): array // Get schema to determine types $seeder = app(BlueprintSeeder::class); $schema = $seeder->getSchema(); - + $rules = []; foreach ($schema['flags'] as $key => $config) { $flagPath = "flags:{$key}"; - + // Build validation rules based on type switch ($config['type']) { case 'boolean': @@ -62,7 +69,7 @@ public function rules(): array break; } } - + return $rules; } -} \ No newline at end of file +} diff --git a/app/Http/Controllers/Admin/ExtensionsController.php b/app/Http/Controllers/Admin/ExtensionsController.php index 694d50d4..083d8c64 100644 --- a/app/Http/Controllers/Admin/ExtensionsController.php +++ b/app/Http/Controllers/Admin/ExtensionsController.php @@ -2,14 +2,15 @@ namespace Pterodactyl\Http\Controllers\Admin; -use Illuminate\Support\Facades\Artisan; use Illuminate\View\View; +use Database\Seeders\BlueprintSeeder; +use Illuminate\Support\Facades\Artisan; use Illuminate\View\Factory as ViewFactory; use Pterodactyl\Http\Controllers\Controller; +use Pterodactyl\Models\ExtensionCachedMetadata; use Pterodactyl\Services\Helpers\SoftwareVersionService; use Pterodactyl\BlueprintFramework\Services\PlaceholderService\BlueprintPlaceholderService; use Pterodactyl\BlueprintFramework\Libraries\ExtensionLibrary\Admin\BlueprintAdminLibrary as BlueprintExtensionLibrary; -use Database\Seeders\BlueprintSeeder; class ExtensionsController extends Controller { @@ -32,7 +33,7 @@ public function index(): View { $configuration = $this->blueprint->dbGetMany('blueprint'); $defaults = []; - + if (($configuration['internal:version:latest'] ?? false) === false) { Artisan::call('bp:version:cache'); $latestBlueprintVersion = $this->blueprint->dbGet('blueprint', 'internal:version:latest'); @@ -47,6 +48,15 @@ public function index(): View } } + $metadata = []; + if($this->blueprint->dbGet('blueprint', 'flags:remote_metadata')) { + $metadata = ExtensionCachedMetadata::whereIn('identifier', $this->blueprint->extensions()) + ->get() + ->keyBy('identifier') + ->map(fn($m) => $m->metadata) + ->toArray(); + } + return $this->view->make('admin.extensions', [ 'blueprint' => $this->blueprint, 'PlaceholderService' => $this->PlaceholderService, @@ -54,7 +64,8 @@ public function index(): View 'latestBlueprintVersion' => $latestBlueprintVersion, 'defaults' => $defaults, 'seeder' => $this->seeder, - + 'metadata' => $metadata, + 'version' => $this->version, 'root' => "/admin/extensions", ]); diff --git a/app/Models/ExtensionCachedMetadata.php b/app/Models/ExtensionCachedMetadata.php new file mode 100644 index 00000000..cfc0e9c4 --- /dev/null +++ b/app/Models/ExtensionCachedMetadata.php @@ -0,0 +1,26 @@ + 'array', + 'fetched_at' => 'datetime', + ]; + protected $fillable = ['identifier', 'metadata', 'fetched_at']; + public $timestamps = true; + + // return the latest_version for a given extension identifier, or null if not found + public static function latestVersionFor(string $identifier): ?string + { + $row = static::where('identifier', $identifier)->first(['metadata']); + + if (! $row) { + return null; + } + + return $row->metadata['latest_version'] ?? null; + } +} diff --git a/blueprint/extensions/blueprint/assets/blueprint.style.css b/blueprint/extensions/blueprint/assets/blueprint.style.css index e4cb0060..e4f5ab73 100644 --- a/blueprint/extensions/blueprint/assets/blueprint.style.css +++ b/blueprint/extensions/blueprint/assets/blueprint.style.css @@ -25,9 +25,9 @@ tag { display:inline-block; - padding:3px; - background-color:#505050; - border-radius:5px; + padding: 3px 5px; + background-color: #4d5b69; + border-radius:15px; font-size:12px; color:white; } @@ -36,7 +36,7 @@ tag[mg-right] {margin-right:5px;} tag[red] {background-color:#ff4040;} tag[green] {background-color:#27b949;} tag[blue] {background-color:#288afb;} -[ext-title]{display:flex; flex-direction:row; align-items:center;} +[ext-title]{display:flex; flex-direction:row; align-items: end;} .btn-gray { @@ -111,7 +111,18 @@ tag[blue] {background-color:#288afb;} width: calc( 100% - 15px ); overflow: clip; text-overflow: ellipsis; - opacity: .6; +} +.extension-btn-update { + background-color: #194323; + color: #3dd15f !important; + border: 1px solid #265f33; + font-weight: 700; + border-radius: 12px; + padding: 0 8px; + display: inline-flex; + flex-direction: row; + gap: 5px; + margin-left: 5px; } .extension-btn { background-color:#1f2933; @@ -120,7 +131,6 @@ tag[blue] {background-color:#288afb;} height:calc(65px + 14px); padding: 0px !important; overflow:hidden; - vertical-align:center; transition:background-color .2s; border-radius:8px; } @@ -169,9 +179,15 @@ tag[blue] {background-color:#288afb;} margin-left: -7px; border-width: 7px; border-style: solid; - border-color: + border-color: transparent transparent #1f2933 transparent; -} \ No newline at end of file +} + +@media screen and (width <= 600px) { + .blueprint-extension-title-tag-icon { + display: none; + } +} diff --git a/database/Seeders/BlueprintSeeder.php b/database/Seeders/BlueprintSeeder.php index bc28defe..f913b94d 100644 --- a/database/Seeders/BlueprintSeeder.php +++ b/database/Seeders/BlueprintSeeder.php @@ -45,6 +45,11 @@ class BlueprintSeeder extends Seeder 'type' => 'boolean', 'hidden' => false, ], + 'remote_metadata' => [ + 'default' => true, + 'type' => 'boolean', + 'hidden' => false, + ], 'show_in_sidebar' => [ 'default' => false, 'type' => 'boolean', diff --git a/database/migrations/2026_05_10_174849_create_extension_cached_metadata_table.php b/database/migrations/2026_05_10_174849_create_extension_cached_metadata_table.php new file mode 100644 index 00000000..9fdbafc6 --- /dev/null +++ b/database/migrations/2026_05_10_174849_create_extension_cached_metadata_table.php @@ -0,0 +1,26 @@ +id(); + $table->string('identifier')->unique(); // the extensions' identifier + $table->json('metadata'); // the metadata related to the extension + $table->timestamp('fetched_at')->nullable(); // last time blueprint fetched ts + $table->timestamps(); + + $table->index(['fetched_at']); + }); + } + + public function down(): void + { + Schema::dropIfExists('extension_cached_metadata'); + } +}; diff --git a/resources/views/admin/extensions.blade.php b/resources/views/admin/extensions.blade.php index fa63d1e3..f1138eb0 100644 --- a/resources/views/admin/extensions.blade.php +++ b/resources/views/admin/extensions.blade.php @@ -131,7 +131,7 @@ logo logo

Blueprint

-

+

system @@ -143,11 +143,18 @@ @foreach($blueprint->extensionsConfigs() as $extension) - + @include("blueprint.admin.entry", [ 'EXTENSION_ID' => $extension['identifier'], 'EXTENSION_NAME' => $extension['name'], 'EXTENSION_VERSION' => $extension['version'], + 'EXTENSION_METADATA' => $metadataScoped, 'EXTENSION_ICON' => !empty($extension['icon']) ? '/assets/extensions/'.$extension['identifier'].'/icon.'.pathinfo($extension['icon'], PATHINFO_EXTENSION) : '/assets/extensions/'.$extension['identifier'].'/icon.jpg' diff --git a/resources/views/blueprint/admin/admin.blade.php b/resources/views/blueprint/admin/admin.blade.php index a7efe441..0f6fbb07 100644 --- a/resources/views/blueprint/admin/admin.blade.php +++ b/resources/views/blueprint/admin/admin.blade.php @@ -12,7 +12,7 @@ @endsection @section("blueprint.import") - {!! $blueprint->importStylesheet('https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css') !!} + {!! $blueprint->importStylesheet('https://cdn.jsdelivr.net/npm/bootstrap-icons@1.13.1/font/bootstrap-icons.css') !!} {!! $blueprint->importStylesheet('/assets/extensions/blueprint/admin.extensions.css') !!} {!! $blueprint->importStylesheet('/assets/extensions/blueprint/blueprint.style.css') !!} @endsection diff --git a/resources/views/blueprint/admin/entry.blade.php b/resources/views/blueprint/admin/entry.blade.php index ab5f3509..247a4e6f 100644 --- a/resources/views/blueprint/admin/entry.blade.php +++ b/resources/views/blueprint/admin/entry.blade.php @@ -1,4 +1,16 @@ @if(isset($EXTENSION_ID)) + @php + $latest = true; + if(isset($EXTENSION_METADATA)) { + if( + $EXTENSION_METADATA['latest_version'] != $EXTENSION_VERSION + && $EXTENSION_METADATA['local_version'] == $EXTENSION_VERSION + ) { + $latest = false; + } + } + @endphp +

-@endif \ No newline at end of file +@endif diff --git a/resources/views/blueprint/admin/template.blade.php b/resources/views/blueprint/admin/template.blade.php index fc27feeb..c8128d71 100644 --- a/resources/views/blueprint/admin/template.blade.php +++ b/resources/views/blueprint/admin/template.blade.php @@ -13,7 +13,30 @@ @endif -

{{ $EXTENSION_NAME }}{{ $EXTENSION_VERSION }}

+

+ {{ $EXTENSION_NAME }} + + {{ $EXTENSION_VERSION }} + + + extensionMetadata($EXTENSION_ID); + if(isset($meta)) { + if( + $meta['latest_version'] != $EXTENSION_VERSION + && $meta['local_version'] == $EXTENSION_VERSION + ) { + $latest = false; + } + } + ?> + @if(!$latest) + + {{ $meta['latest_version'] }} + + @endif +

@endsection @section("extension.description")