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 @@
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 }}
+
+
+
@endsection
@section("extension.description")