From 0b2b5bf25fdb578ed40c2e5dbeb0c81f87b4d631 Mon Sep 17 00:00:00 2001 From: dowjames Date: Sat, 27 Dec 2025 16:24:18 -0500 Subject: [PATCH 001/104] Update holidays-ical.blade.php *Past events are removed. *Events that started earlier but are still ongoing today remain visible. *Anything from today onward displays. --- .../views/recipes/holidays-ical.blade.php | 30 ++++++++++++------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/resources/views/recipes/holidays-ical.blade.php b/resources/views/recipes/holidays-ical.blade.php index f5f5403..454709d 100644 --- a/resources/views/recipes/holidays-ical.blade.php +++ b/resources/views/recipes/holidays-ical.blade.php @@ -2,36 +2,46 @@ @php use Carbon\Carbon; + $today = Carbon::today(config('app.timezone')); + $events = collect($data['ical'] ?? []) ->map(function (array $event): array { - $start = null; - $end = null; - try { - $start = isset($event['DTSTART']) ? Carbon::parse($event['DTSTART'])->setTimezone(config('app.timezone')) : null; + $start = isset($event['DTSTART']) + ? Carbon::parse($event['DTSTART'])->setTimezone(config('app.timezone')) + : null; } catch (Exception $e) { $start = null; } try { - $end = isset($event['DTEND']) ? Carbon::parse($event['DTEND'])->setTimezone(config('app.timezone')) : null; + $end = isset($event['DTEND']) + ? Carbon::parse($event['DTEND'])->setTimezone(config('app.timezone')) + : null; } catch (Exception $e) { $end = null; } return [ - 'summary' => $event['SUMMARY'] ?? 'Untitled event', - 'location' => $event['LOCATION'] ?? null, - 'start' => $start, - 'end' => $end, + 'summary' => $event['SUMMARY'] ?? 'Untitled event', + 'location' => $event['LOCATION'] ?? '—', + 'start' => $start, + 'end' => $end, ]; }) - ->filter(fn ($event) => $event['start']) + ->filter(fn ($event) => + $event['start'] && + ( + $event['start']->greaterThanOrEqualTo($today) || + ($event['end'] && $event['end']->greaterThanOrEqualTo($today)) + ) + ) ->sortBy('start') ->take($size === 'quadrant' ? 5 : 8) ->values(); @endphp + From d81c1b99f1611ac05ab4be63fd36acec8c56ff5e Mon Sep 17 00:00:00 2001 From: Benjamin Nussbaum Date: Mon, 29 Dec 2025 11:39:21 +0100 Subject: [PATCH 002/104] Update download and star counts in README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 20bae5d..34f5c3d 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ [![tests](https://github.com/usetrmnl/byos_laravel/actions/workflows/test.yml/badge.svg)](https://github.com/usetrmnl/byos_laravel/actions/workflows/test.yml) TRMNL BYOS Laravel is a self-hostable implementation of a TRMNL server, built with Laravel. -It allows you to manage TRMNL devices, generate screens using **native plugins** (Screens API, Markup), **recipes** (100+ from the [OSS community catalog](https://bnussbau.github.io/trmnl-recipe-catalog/), ~500 from the [TRMNL catalog](https://usetrmnl.com/recipes), or your own), or the **API**, and can also act as a **proxy** for the native cloud service (Core). With over 30k downloads and 130+ stars, it’s the most popular community-driven BYOS. +It allows you to manage TRMNL devices, generate screens using **native plugins** (Screens API, Markup), **recipes** (100+ from the [OSS community catalog](https://bnussbau.github.io/trmnl-recipe-catalog/), ~500 from the [TRMNL catalog](https://usetrmnl.com/recipes), or your own), or the **API**, and can also act as a **proxy** for the native cloud service (Core). With over 35k downloads and 150+ stars, it’s the most popular community-driven BYOS. ![Screenshot](README_byos-screenshot.png) ![Screenshot](README_byos-screenshot-dark.png) From d4b5cf99d578421c1459c90a68fccd52902599b8 Mon Sep 17 00:00:00 2001 From: Benjamin Nussbaum Date: Mon, 29 Dec 2025 22:05:20 +0100 Subject: [PATCH 003/104] chore: update dependencies --- composer.lock | 100 +++++++++++++++++++++++++------------------------- 1 file changed, 50 insertions(+), 50 deletions(-) diff --git a/composer.lock b/composer.lock index a2e83af..1b578bf 100644 --- a/composer.lock +++ b/composer.lock @@ -62,16 +62,16 @@ }, { "name": "aws/aws-sdk-php", - "version": "3.369.1", + "version": "3.369.4", "source": { "type": "git", "url": "https://github.com/aws/aws-sdk-php.git", - "reference": "b2d04cf1184a96839a8ab62ec6e3cf2d62a278e3" + "reference": "2aa1ef195e90140d733382e4341732ce113024f5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/b2d04cf1184a96839a8ab62ec6e3cf2d62a278e3", - "reference": "b2d04cf1184a96839a8ab62ec6e3cf2d62a278e3", + "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/2aa1ef195e90140d733382e4341732ce113024f5", + "reference": "2aa1ef195e90140d733382e4341732ce113024f5", "shasum": "" }, "require": { @@ -85,7 +85,7 @@ "mtdowling/jmespath.php": "^2.8.0", "php": ">=8.1", "psr/http-message": "^1.0 || ^2.0", - "symfony/filesystem": "^v6.4.3 || ^v7.1.0 || ^v8.0.0" + "symfony/filesystem": "^v5.4.45 || ^v6.4.3 || ^v7.1.0 || ^v8.0.0" }, "require-dev": { "andrewsville/php-token-reflection": "^1.4", @@ -153,9 +153,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.369.1" + "source": "https://github.com/aws/aws-sdk-php/tree/3.369.4" }, - "time": "2025-12-22T19:13:21+00:00" + "time": "2025-12-29T19:07:47+00:00" }, { "name": "bnussbau/laravel-trmnl-blade", @@ -950,24 +950,24 @@ }, { "name": "graham-campbell/result-type", - "version": "v1.1.3", + "version": "v1.1.4", "source": { "type": "git", "url": "https://github.com/GrahamCampbell/Result-Type.git", - "reference": "3ba905c11371512af9d9bdd27d99b782216b6945" + "reference": "e01f4a821471308ba86aa202fed6698b6b695e3b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/GrahamCampbell/Result-Type/zipball/3ba905c11371512af9d9bdd27d99b782216b6945", - "reference": "3ba905c11371512af9d9bdd27d99b782216b6945", + "url": "https://api.github.com/repos/GrahamCampbell/Result-Type/zipball/e01f4a821471308ba86aa202fed6698b6b695e3b", + "reference": "e01f4a821471308ba86aa202fed6698b6b695e3b", "shasum": "" }, "require": { "php": "^7.2.5 || ^8.0", - "phpoption/phpoption": "^1.9.3" + "phpoption/phpoption": "^1.9.5" }, "require-dev": { - "phpunit/phpunit": "^8.5.39 || ^9.6.20 || ^10.5.28" + "phpunit/phpunit": "^8.5.41 || ^9.6.22 || ^10.5.45 || ^11.5.7" }, "type": "library", "autoload": { @@ -996,7 +996,7 @@ ], "support": { "issues": "https://github.com/GrahamCampbell/Result-Type/issues", - "source": "https://github.com/GrahamCampbell/Result-Type/tree/v1.1.3" + "source": "https://github.com/GrahamCampbell/Result-Type/tree/v1.1.4" }, "funding": [ { @@ -1008,7 +1008,7 @@ "type": "tidelift" } ], - "time": "2024-07-20T21:45:45+00:00" + "time": "2025-12-27T19:43:20+00:00" }, { "name": "guzzlehttp/guzzle", @@ -1617,16 +1617,16 @@ }, { "name": "laravel/framework", - "version": "v12.43.1", + "version": "v12.44.0", "source": { "type": "git", "url": "https://github.com/laravel/framework.git", - "reference": "195b893593a9298edee177c0844132ebaa02102f" + "reference": "592bbf1c036042958332eb98e3e8131b29102f33" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/framework/zipball/195b893593a9298edee177c0844132ebaa02102f", - "reference": "195b893593a9298edee177c0844132ebaa02102f", + "url": "https://api.github.com/repos/laravel/framework/zipball/592bbf1c036042958332eb98e3e8131b29102f33", + "reference": "592bbf1c036042958332eb98e3e8131b29102f33", "shasum": "" }, "require": { @@ -1835,7 +1835,7 @@ "issues": "https://github.com/laravel/framework/issues", "source": "https://github.com/laravel/framework" }, - "time": "2025-12-16T18:53:08+00:00" + "time": "2025-12-23T15:29:43+00:00" }, { "name": "laravel/prompts", @@ -3885,16 +3885,16 @@ }, { "name": "phpoption/phpoption", - "version": "1.9.4", + "version": "1.9.5", "source": { "type": "git", "url": "https://github.com/schmittjoh/php-option.git", - "reference": "638a154f8d4ee6a5cfa96d6a34dfbe0cffa9566d" + "reference": "75365b91986c2405cf5e1e012c5595cd487a98be" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/schmittjoh/php-option/zipball/638a154f8d4ee6a5cfa96d6a34dfbe0cffa9566d", - "reference": "638a154f8d4ee6a5cfa96d6a34dfbe0cffa9566d", + "url": "https://api.github.com/repos/schmittjoh/php-option/zipball/75365b91986c2405cf5e1e012c5595cd487a98be", + "reference": "75365b91986c2405cf5e1e012c5595cd487a98be", "shasum": "" }, "require": { @@ -3944,7 +3944,7 @@ ], "support": { "issues": "https://github.com/schmittjoh/php-option/issues", - "source": "https://github.com/schmittjoh/php-option/tree/1.9.4" + "source": "https://github.com/schmittjoh/php-option/tree/1.9.5" }, "funding": [ { @@ -3956,7 +3956,7 @@ "type": "tidelift" } ], - "time": "2025-08-21T11:53:16+00:00" + "time": "2025-12-27T19:41:33+00:00" }, { "name": "phpseclib/phpseclib", @@ -7723,26 +7723,26 @@ }, { "name": "vlucas/phpdotenv", - "version": "v5.6.2", + "version": "v5.6.3", "source": { "type": "git", "url": "https://github.com/vlucas/phpdotenv.git", - "reference": "24ac4c74f91ee2c193fa1aaa5c249cb0822809af" + "reference": "955e7815d677a3eaa7075231212f2110983adecc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/24ac4c74f91ee2c193fa1aaa5c249cb0822809af", - "reference": "24ac4c74f91ee2c193fa1aaa5c249cb0822809af", + "url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/955e7815d677a3eaa7075231212f2110983adecc", + "reference": "955e7815d677a3eaa7075231212f2110983adecc", "shasum": "" }, "require": { "ext-pcre": "*", - "graham-campbell/result-type": "^1.1.3", + "graham-campbell/result-type": "^1.1.4", "php": "^7.2.5 || ^8.0", - "phpoption/phpoption": "^1.9.3", - "symfony/polyfill-ctype": "^1.24", - "symfony/polyfill-mbstring": "^1.24", - "symfony/polyfill-php80": "^1.24" + "phpoption/phpoption": "^1.9.5", + "symfony/polyfill-ctype": "^1.26", + "symfony/polyfill-mbstring": "^1.26", + "symfony/polyfill-php80": "^1.26" }, "require-dev": { "bamarni/composer-bin-plugin": "^1.8.2", @@ -7791,7 +7791,7 @@ ], "support": { "issues": "https://github.com/vlucas/phpdotenv/issues", - "source": "https://github.com/vlucas/phpdotenv/tree/v5.6.2" + "source": "https://github.com/vlucas/phpdotenv/tree/v5.6.3" }, "funding": [ { @@ -7803,7 +7803,7 @@ "type": "tidelift" } ], - "time": "2025-04-30T23:37:27+00:00" + "time": "2025-12-27T19:49:13+00:00" }, { "name": "voku/portable-ascii", @@ -10122,16 +10122,16 @@ }, { "name": "phpunit/php-code-coverage", - "version": "12.5.1", + "version": "12.5.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "c467c59a4f6e04b942be422844e7a6352fa01b57" + "reference": "4a9739b51cbcb355f6e95659612f92e282a7077b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/c467c59a4f6e04b942be422844e7a6352fa01b57", - "reference": "c467c59a4f6e04b942be422844e7a6352fa01b57", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/4a9739b51cbcb355f6e95659612f92e282a7077b", + "reference": "4a9739b51cbcb355f6e95659612f92e282a7077b", "shasum": "" }, "require": { @@ -10146,7 +10146,7 @@ "sebastian/environment": "^8.0.3", "sebastian/lines-of-code": "^4.0", "sebastian/version": "^6.0", - "theseer/tokenizer": "^2.0" + "theseer/tokenizer": "^2.0.1" }, "require-dev": { "phpunit/phpunit": "^12.5.1" @@ -10187,7 +10187,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", - "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/12.5.1" + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/12.5.2" }, "funding": [ { @@ -10207,7 +10207,7 @@ "type": "tidelift" } ], - "time": "2025-12-08T07:17:58+00:00" + "time": "2025-12-24T07:03:04+00:00" }, { "name": "phpunit/php-file-iterator", @@ -10561,16 +10561,16 @@ }, { "name": "rector/rector", - "version": "2.2.14", + "version": "2.3.0", "source": { "type": "git", "url": "https://github.com/rectorphp/rector.git", - "reference": "6d56bb0e94d4df4f57a78610550ac76ab403657d" + "reference": "f7166355dcf47482f27be59169b0825995f51c7d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/rectorphp/rector/zipball/6d56bb0e94d4df4f57a78610550ac76ab403657d", - "reference": "6d56bb0e94d4df4f57a78610550ac76ab403657d", + "url": "https://api.github.com/repos/rectorphp/rector/zipball/f7166355dcf47482f27be59169b0825995f51c7d", + "reference": "f7166355dcf47482f27be59169b0825995f51c7d", "shasum": "" }, "require": { @@ -10609,7 +10609,7 @@ ], "support": { "issues": "https://github.com/rectorphp/rector/issues", - "source": "https://github.com/rectorphp/rector/tree/2.2.14" + "source": "https://github.com/rectorphp/rector/tree/2.3.0" }, "funding": [ { @@ -10617,7 +10617,7 @@ "type": "github" } ], - "time": "2025-12-09T10:57:55+00:00" + "time": "2025-12-25T22:00:18+00:00" }, { "name": "sebastian/cli-parser", From e6d66af2984c0750a7f76e62c2529fb9bb091f20 Mon Sep 17 00:00:00 2001 From: Benjamin Nussbaum Date: Mon, 29 Dec 2025 22:16:29 +0100 Subject: [PATCH 004/104] fix(#135): use user configured timezone in Playlists --- app/Models/Playlist.php | 24 +++++++++---- tests/Feature/PlaylistSchedulingTest.php | 45 ++++++++++++++++++++++++ 2 files changed, 63 insertions(+), 6 deletions(-) diff --git a/app/Models/Playlist.php b/app/Models/Playlist.php index 7b55a73..68fbddb 100644 --- a/app/Models/Playlist.php +++ b/app/Models/Playlist.php @@ -6,6 +6,7 @@ use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\HasMany; +use Illuminate\Support\Carbon; class Playlist extends Model { @@ -37,21 +38,32 @@ class Playlist extends Model return false; } - // Check weekday - if ($this->weekdays !== null && ! in_array(now()->dayOfWeek, $this->weekdays)) { + // Get user's timezone or fall back to app timezone + $timezone = $this->device->user->timezone ?? config('app.timezone'); + $now = now($timezone); + + // Check weekday (using timezone-aware time) + if ($this->weekdays !== null && ! in_array($now->dayOfWeek, $this->weekdays)) { return false; } if ($this->active_from !== null && $this->active_until !== null) { - $now = now(); + // Create timezone-aware datetime objects for active_from and active_until + $activeFrom = $now->copy() + ->setTimeFrom($this->active_from) + ->timezone($timezone); + + $activeUntil = $now->copy() + ->setTimeFrom($this->active_until) + ->timezone($timezone); // Handle time ranges that span across midnight - if ($this->active_from > $this->active_until) { + if ($activeFrom > $activeUntil) { // Time range spans midnight (e.g., 09:01 to 03:58) - if ($now >= $this->active_from || $now <= $this->active_until) { + if ($now >= $activeFrom || $now <= $activeUntil) { return true; } - } elseif ($now >= $this->active_from && $now <= $this->active_until) { + } elseif ($now >= $activeFrom && $now <= $activeUntil) { return true; } diff --git a/tests/Feature/PlaylistSchedulingTest.php b/tests/Feature/PlaylistSchedulingTest.php index aea4923..18d0032 100644 --- a/tests/Feature/PlaylistSchedulingTest.php +++ b/tests/Feature/PlaylistSchedulingTest.php @@ -130,3 +130,48 @@ test('playlist isActiveNow handles normal time ranges correctly', function (): v Carbon::setTestNow(Carbon::create(2024, 1, 1, 20, 0, 0)); expect($playlist->isActiveNow())->toBeFalse(); }); + +test('playlist scheduling respects user timezone preference', function (): void { + // Create a user with a timezone that's UTC+1 (e.g., Europe/Berlin) + // This simulates the bug where setting 00:15 doesn't work until one hour later + $user = User::factory()->create([ + 'timezone' => 'Europe/Berlin', // UTC+1 in winter, UTC+2 in summer + ]); + + $device = Device::factory()->create(['user_id' => $user->id]); + + // Create a playlist that should be active from 00:15 to 01:00 in the user's timezone + $playlist = Playlist::factory()->create([ + 'device_id' => $device->id, + 'is_active' => true, + 'active_from' => '00:15', + 'active_until' => '01:00', + 'weekdays' => null, + ]); + + // Set test time to 00:15 in the user's timezone (Europe/Berlin) + // In January, Europe/Berlin is UTC+1, so 00:15 Berlin time = 23:15 UTC the previous day + // But Carbon::setTestNow uses UTC by default, so we need to set it to the UTC equivalent + // For January 1, 2024 at 00:15 Berlin time (UTC+1), that's December 31, 2023 at 23:15 UTC + $berlinTime = Carbon::create(2024, 1, 1, 0, 15, 0, 'Europe/Berlin'); + Carbon::setTestNow($berlinTime->utc()); + + // The playlist should be active at 00:15 in the user's timezone + // This test should pass after the fix, but will fail with the current bug + expect($playlist->isActiveNow())->toBeTrue(); + + // Test at 00:30 in user's timezone - should still be active + $berlinTime = Carbon::create(2024, 1, 1, 0, 30, 0, 'Europe/Berlin'); + Carbon::setTestNow($berlinTime->utc()); + expect($playlist->isActiveNow())->toBeTrue(); + + // Test at 01:15 in user's timezone - should NOT be active (past the end time) + $berlinTime = Carbon::create(2024, 1, 1, 1, 15, 0, 'Europe/Berlin'); + Carbon::setTestNow($berlinTime->utc()); + expect($playlist->isActiveNow())->toBeFalse(); + + // Test at 00:10 in user's timezone - should NOT be active (before start time) + $berlinTime = Carbon::create(2024, 1, 1, 0, 10, 0, 'Europe/Berlin'); + Carbon::setTestNow($berlinTime->utc()); + expect($playlist->isActiveNow())->toBeFalse(); +}); From a5cb38421ebafa4e4c34916a84102fe3c9fa3c52 Mon Sep 17 00:00:00 2001 From: Benjamin Nussbaum Date: Mon, 29 Dec 2025 22:24:32 +0100 Subject: [PATCH 005/104] fix(#131): invalidate cache when updating recipe markup --- app/Models/Plugin.php | 7 ++++++ tests/Feature/ImageGenerationServiceTest.php | 24 ++++++++++++++++++++ 2 files changed, 31 insertions(+) diff --git a/app/Models/Plugin.php b/app/Models/Plugin.php index 2915247..9132d6c 100644 --- a/app/Models/Plugin.php +++ b/app/Models/Plugin.php @@ -55,6 +55,13 @@ class Plugin extends Model $model->uuid = Str::uuid(); } }); + + static::updating(function ($model): void { + // Reset image cache when markup changes + if ($model->isDirty('render_markup')) { + $model->current_image = null; + } + }); } public function user() diff --git a/tests/Feature/ImageGenerationServiceTest.php b/tests/Feature/ImageGenerationServiceTest.php index 603205e..07bb6a6 100644 --- a/tests/Feature/ImageGenerationServiceTest.php +++ b/tests/Feature/ImageGenerationServiceTest.php @@ -324,6 +324,30 @@ it('resetIfNotCacheable preserves image for standard devices', function (): void expect($plugin->current_image)->toBe('test-uuid'); }); +it('cache is reset when plugin markup changes', function (): void { + // Create a plugin with cached image + $plugin = App\Models\Plugin::factory()->create([ + 'current_image' => 'cached-uuid', + 'render_markup' => '
Original markup
', + ]); + + // Create devices with standard dimensions (cacheable) + Device::factory()->count(2)->create([ + 'width' => 800, + 'height' => 480, + 'rotate' => 0, + ]); + + // Update the plugin markup + $plugin->update([ + 'render_markup' => '
Updated markup
', + ]); + + // Assert cache was reset when markup changed + $plugin->refresh(); + expect($plugin->current_image)->toBeNull(); +}); + it('determines correct image format from device model', function (): void { // Test BMP format detection $bmpModel = DeviceModel::factory()->create([ From 1298814521b1ee27e955867364d9d9fdd597e9a2 Mon Sep 17 00:00:00 2001 From: Benjamin Nussbaum Date: Mon, 29 Dec 2025 23:07:21 +0100 Subject: [PATCH 006/104] fix(#136): mac address matching is case senstive --- app/Models/Device.php | 8 +++ routes/api.php | 10 ++-- tests/Feature/Api/DeviceEndpointsTest.php | 69 +++++++++++++++++++++++ 3 files changed, 82 insertions(+), 5 deletions(-) diff --git a/app/Models/Device.php b/app/Models/Device.php index 2eeb25b..3583f48 100644 --- a/app/Models/Device.php +++ b/app/Models/Device.php @@ -20,6 +20,14 @@ class Device extends Model protected $guarded = ['id']; + /** + * Set the MAC address attribute, normalizing to uppercase. + */ + public function setMacAddressAttribute(?string $value): void + { + $this->attributes['mac_address'] = $value ? mb_strtoupper($value) : null; + } + protected $casts = [ 'battery_notification_sent' => 'boolean', 'proxy_cloud' => 'boolean', diff --git a/routes/api.php b/routes/api.php index 9721a0f..d1dbcac 100644 --- a/routes/api.php +++ b/routes/api.php @@ -18,7 +18,7 @@ use Illuminate\Support\Str; Route::get('/display', function (Request $request) { $mac_address = $request->header('id'); $access_token = $request->header('access-token'); - $device = Device::where('mac_address', $mac_address) + $device = Device::where('mac_address', mb_strtoupper($mac_address ?? '')) ->where('api_key', $access_token) ->first(); @@ -29,7 +29,7 @@ Route::get('/display', function (Request $request) { if ($auto_assign_user) { // Create a new device and assign it to this user $device = Device::create([ - 'mac_address' => $mac_address, + 'mac_address' => mb_strtoupper($mac_address ?? ''), 'api_key' => $access_token, 'user_id' => $auto_assign_user->id, 'name' => "{$auto_assign_user->name}'s TRMNL", @@ -204,7 +204,7 @@ Route::get('/setup', function (Request $request) { ], 404); } - $device = Device::where('mac_address', $mac_address)->first(); + $device = Device::where('mac_address', mb_strtoupper($mac_address))->first(); if (! $device) { // Check if there's a user with assign_new_devices enabled @@ -219,7 +219,7 @@ Route::get('/setup', function (Request $request) { // Create a new device and assign it to this user $device = Device::create([ - 'mac_address' => $mac_address, + 'mac_address' => mb_strtoupper($mac_address), 'api_key' => Str::random(22), 'user_id' => $auto_assign_user->id, 'name' => "{$auto_assign_user->name}'s TRMNL", @@ -345,7 +345,7 @@ Route::post('/display/update', function (Request $request) { Route::post('/screens', function (Request $request) { $mac_address = $request->header('id'); $access_token = $request->header('access-token'); - $device = Device::where('mac_address', $mac_address) + $device = Device::where('mac_address', mb_strtoupper($mac_address ?? '')) ->where('api_key', $access_token) ->first(); diff --git a/tests/Feature/Api/DeviceEndpointsTest.php b/tests/Feature/Api/DeviceEndpointsTest.php index 726f313..aff6758 100644 --- a/tests/Feature/Api/DeviceEndpointsTest.php +++ b/tests/Feature/Api/DeviceEndpointsTest.php @@ -954,3 +954,72 @@ test('setup endpoint handles non-existent device model gracefully', function (): expect($device)->not->toBeNull() ->and($device->device_model_id)->toBeNull(); }); + +test('setup endpoint matches MAC address case-insensitively', function (): void { + // Create device with lowercase MAC address + $device = Device::factory()->create([ + 'mac_address' => 'a1:b2:c3:d4:e5:f6', + 'api_key' => 'test-api-key', + 'friendly_id' => 'test-device', + ]); + + // Request with uppercase MAC address should still match + $response = $this->withHeaders([ + 'id' => 'A1:B2:C3:D4:E5:F6', + ])->get('/api/setup'); + + $response->assertOk() + ->assertJson([ + 'status' => 200, + 'api_key' => 'test-api-key', + 'friendly_id' => 'test-device', + 'message' => 'Welcome to TRMNL BYOS', + ]); +}); + +test('display endpoint matches MAC address case-insensitively', function (): void { + // Create device with lowercase MAC address + $device = Device::factory()->create([ + 'mac_address' => 'a1:b2:c3:d4:e5:f6', + 'api_key' => 'test-api-key', + 'current_screen_image' => 'test-image', + ]); + + // Request with uppercase MAC address should still match + $response = $this->withHeaders([ + 'id' => 'A1:B2:C3:D4:E5:F6', + 'access-token' => $device->api_key, + 'rssi' => -70, + 'battery_voltage' => 3.8, + 'fw-version' => '1.0.0', + ])->get('/api/display'); + + $response->assertOk() + ->assertJson([ + 'status' => '0', + 'filename' => 'test-image.bmp', + ]); +}); + +test('screens endpoint matches MAC address case-insensitively', function (): void { + Queue::fake(); + + // Create device with uppercase MAC address + $device = Device::factory()->create([ + 'mac_address' => 'A1:B2:C3:D4:E5:F6', + 'api_key' => 'test-api-key', + ]); + + // Request with lowercase MAC address should still match + $response = $this->withHeaders([ + 'id' => 'a1:b2:c3:d4:e5:f6', + 'access-token' => $device->api_key, + ])->post('/api/screens', [ + 'image' => [ + 'content' => '
Test content
', + ], + ]); + + $response->assertOk(); + Queue::assertPushed(GenerateScreenJob::class); +}); From 3cdc2678093b6efb260ae434260dbb699396b99e Mon Sep 17 00:00:00 2001 From: Benjamin Nussbaum Date: Mon, 29 Dec 2025 23:08:52 +0100 Subject: [PATCH 007/104] chore: pint --- app/Models/Playlist.php | 1 - app/Services/Plugin/Parsers/IcalResponseParser.php | 2 +- database/factories/DevicePaletteFactory.php | 2 +- 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/app/Models/Playlist.php b/app/Models/Playlist.php index 68fbddb..b4daf5e 100644 --- a/app/Models/Playlist.php +++ b/app/Models/Playlist.php @@ -6,7 +6,6 @@ use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\HasMany; -use Illuminate\Support\Carbon; class Playlist extends Model { diff --git a/app/Services/Plugin/Parsers/IcalResponseParser.php b/app/Services/Plugin/Parsers/IcalResponseParser.php index f87e71c..c8f2b58 100644 --- a/app/Services/Plugin/Parsers/IcalResponseParser.php +++ b/app/Services/Plugin/Parsers/IcalResponseParser.php @@ -34,7 +34,7 @@ class IcalResponseParser implements ResponseParser $filteredEvents = array_values(array_filter($events, function (array $event) use ($windowStart, $windowEnd): bool { $startDate = $this->asCarbon($event['DTSTART'] ?? null); - if (!$startDate instanceof \Carbon\Carbon) { + if (! $startDate instanceof Carbon) { return false; } diff --git a/database/factories/DevicePaletteFactory.php b/database/factories/DevicePaletteFactory.php index a672873..1d7ed2d 100644 --- a/database/factories/DevicePaletteFactory.php +++ b/database/factories/DevicePaletteFactory.php @@ -20,7 +20,7 @@ class DevicePaletteFactory extends Factory public function definition(): array { return [ - 'id' => 'test-' . $this->faker->unique()->slug(), + 'id' => 'test-'.$this->faker->unique()->slug(), 'name' => $this->faker->words(3, true), 'grays' => $this->faker->randomElement([2, 4, 16, 256]), 'colors' => $this->faker->optional()->passthrough([ From 50853728bcb785244bdf1748647e63c66141e6d4 Mon Sep 17 00:00:00 2001 From: Benjamin Nussbaum Date: Tue, 30 Dec 2025 09:43:05 +0100 Subject: [PATCH 008/104] refactor(#120): remove unnecessary js, improve cache handling --- .../views/livewire/catalog/index.blade.php | 150 +++------ .../views/livewire/catalog/trmnl.blade.php | 303 ++++++++---------- tests/Feature/Livewire/Catalog/IndexTest.php | 83 +++++ tests/Feature/Volt/CatalogTrmnlTest.php | 62 ++++ 4 files changed, 324 insertions(+), 274 deletions(-) diff --git a/resources/views/livewire/catalog/index.blade.php b/resources/views/livewire/catalog/index.blade.php index 83a34fc..3a24b7e 100644 --- a/resources/views/livewire/catalog/index.blade.php +++ b/resources/views/livewire/catalog/index.blade.php @@ -1,20 +1,24 @@ filter(function ($plugin) use ($currentVersion) { // Check if Laravel compatibility is true - if (!Arr::get($plugin, 'byos.byos_laravel.compatibility', false)) { + if (! Arr::get($plugin, 'byos.byos_laravel.compatibility', false)) { return false; } @@ -81,8 +85,9 @@ class extends Component { }) ->sortBy('name') ->toArray(); - } catch (\Exception $e) { - Log::error('Failed to load catalog from URL: ' . $e->getMessage()); + } catch (Exception $e) { + Log::error('Failed to load catalog from URL: '.$e->getMessage()); + return []; } }); @@ -94,8 +99,9 @@ class extends Component { $plugin = collect($this->catalogPlugins)->firstWhere('id', $pluginId); - if (!$plugin || !$plugin['zip_url']) { + if (! $plugin || ! $plugin['zip_url']) { $this->addError('installation', 'Plugin not found or no download URL available.'); + return; } @@ -113,8 +119,8 @@ class extends Component { $this->dispatch('plugin-installed'); Flux::modal('import-from-catalog')->close(); - } catch (\Exception $e) { - $this->addError('installation', 'Error installing plugin: ' . $e->getMessage()); + } catch (Exception $e) { + $this->addError('installation', 'Error installing plugin: '.$e->getMessage()); } finally { $this->installingPlugin = ''; } @@ -124,32 +130,27 @@ class extends Component { { $plugin = collect($this->catalogPlugins)->firstWhere('id', $pluginId); - if (!$plugin) { + if (! $plugin) { $this->addError('preview', 'Plugin not found.'); + return; } $this->previewingPlugin = $pluginId; $this->previewData = $plugin; - - // Store scroll position for restoration later - $this->dispatch('store-scroll-position'); } public function closePreview(): void { $this->previewingPlugin = ''; $this->previewData = []; - - // Restore scroll position when returning to catalog - $this->dispatch('restore-scroll-position'); } }; ?>
@if(empty($catalogPlugins))
- + No plugins available Catalog is empty
@@ -165,25 +166,25 @@ class extends Component { @if($plugin['logo_url']) {{ $plugin['name'] }} @else -
- +
+
@endif
-

{{ $plugin['name'] }}

+ {{ $plugin['name'] }} @if ($plugin['github']) -

by {{ $plugin['github'] }}

+ by {{ $plugin['github'] }} @endif
@if($plugin['license']) - {{ $plugin['license'] }} + {{ $plugin['license'] }} @endif @if($plugin['repo_url']) - + @endif @@ -191,7 +192,7 @@ class extends Component {
@if($plugin['description']) -

{{ $plugin['description'] }}

+ {{ $plugin['description'] }} @endif
@@ -201,14 +202,16 @@ class extends Component { Install - - - Preview - - + @if($plugin['screenshot_url']) + + + Preview + + + @endif @@ -236,34 +239,20 @@ class extends Component {
- @if($previewData['screenshot_url']) -
- Preview of {{ $previewData['name'] }} -
- @elseif($previewData['logo_url']) -
- {{ $previewData['name'] }} logo -

No preview image available

-
- @else -
- -

No preview available

-
- @endif +
+ Preview of {{ $previewData['name'] }} +
@if($previewData['description']) -
-

Description

-

{{ $previewData['description'] }}

+
+ Description + {{ $previewData['description'] }}
@endif -
+
- -@script - -@endscript diff --git a/resources/views/livewire/catalog/trmnl.blade.php b/resources/views/livewire/catalog/trmnl.blade.php index 1b5dd50..dd97e0e 100644 --- a/resources/views/livewire/catalog/trmnl.blade.php +++ b/resources/views/livewire/catalog/trmnl.blade.php @@ -1,20 +1,24 @@ 'newest', ]); - if (!$response->successful()) { - throw new \RuntimeException('Failed to fetch TRMNL recipes'); + if (! $response->successful()) { + throw new RuntimeException('Failed to fetch TRMNL recipes'); } $json = $response->json(); $data = $json['data'] ?? []; + return $this->mapRecipes($data); }); - } catch (\Throwable $e) { - Log::error('TRMNL catalog load error: ' . $e->getMessage()); + } catch (Throwable $e) { + Log::error('TRMNL catalog load error: '.$e->getMessage()); $this->recipes = []; } } @@ -62,23 +67,24 @@ class extends Component { { $this->isSearching = true; try { - $cacheKey = 'trmnl_recipes_search_' . md5($term); + $cacheKey = 'trmnl_recipes_search_'.md5($term); $this->recipes = Cache::remember($cacheKey, 300, function () use ($term) { $response = Http::get('https://usetrmnl.com/recipes.json', [ 'search' => $term, 'sort-by' => 'newest', ]); - if (!$response->successful()) { - throw new \RuntimeException('Failed to search TRMNL recipes'); + if (! $response->successful()) { + throw new RuntimeException('Failed to search TRMNL recipes'); } $json = $response->json(); $data = $json['data'] ?? []; + return $this->mapRecipes($data); }); - } catch (\Throwable $e) { - Log::error('TRMNL catalog search error: ' . $e->getMessage()); + } catch (Throwable $e) { + Log::error('TRMNL catalog search error: '.$e->getMessage()); $this->recipes = []; } finally { $this->isSearching = false; @@ -87,13 +93,14 @@ class extends Component { public function updatedSearch(): void { - $term = trim($this->search); + $term = mb_trim($this->search); if ($term === '') { $this->loadNewest(); + return; } - if (strlen($term) < 2) { + if (mb_strlen($term) < 2) { // Require at least 2 chars to avoid noisy calls return; } @@ -121,62 +128,78 @@ class extends Component { $this->dispatch('plugin-installed'); Flux::modal('import-from-trmnl-catalog')->close(); - } catch (\Exception $e) { - Log::error('Plugin installation failed: ' . $e->getMessage()); - $this->addError('installation', 'Error installing plugin: ' . $e->getMessage()); + } catch (Exception $e) { + Log::error('Plugin installation failed: '.$e->getMessage()); + $this->addError('installation', 'Error installing plugin: '.$e->getMessage()); } } public function previewRecipe(string $recipeId): void { - $recipe = collect($this->recipes)->firstWhere('id', $recipeId); - - if (!$recipe) { - $this->addError('preview', 'Recipe not found.'); - return; - } - $this->previewingRecipe = $recipeId; - $this->previewData = $recipe; - - // Store scroll position for restoration later - $this->dispatch('store-scroll-position'); - } - - public function closePreview(): void - { - $this->previewingRecipe = ''; $this->previewData = []; - // Restore scroll position when returning to catalog - $this->dispatch('restore-scroll-position'); + try { + $response = Http::timeout(10)->get("https://usetrmnl.com/recipes/{$recipeId}.json"); + + if ($response->successful()) { + $item = $response->json()['data'] ?? []; + $this->previewData = $this->mapRecipe($item); + } else { + // Fallback to searching for the specific recipe if single endpoint doesn't exist + $response = Http::timeout(10)->get('https://usetrmnl.com/recipes.json', [ + 'search' => $recipeId, + ]); + + if ($response->successful()) { + $data = $response->json()['data'] ?? []; + $item = collect($data)->firstWhere('id', $recipeId); + if ($item) { + $this->previewData = $this->mapRecipe($item); + } + } + } + } catch (Throwable $e) { + Log::error('TRMNL catalog preview fetch error: '.$e->getMessage()); + } + + if (empty($this->previewData)) { + $this->previewData = collect($this->recipes)->firstWhere('id', $recipeId) ?? []; + } } /** - * @param array> $items + * @param array> $items * @return array> */ private function mapRecipes(array $items): array { return collect($items) - ->map(function (array $item) { - return [ - 'id' => $item['id'] ?? null, - 'name' => $item['name'] ?? 'Untitled', - 'icon_url' => $item['icon_url'] ?? null, - 'screenshot_url' => $item['screenshot_url'] ?? null, - 'author_bio' => is_array($item['author_bio'] ?? null) - ? strip_tags($item['author_bio']['description'] ?? null) - : null, - 'stats' => [ - 'installs' => data_get($item, 'stats.installs'), - 'forks' => data_get($item, 'stats.forks'), - ], - 'detail_url' => isset($item['id']) ? ('https://usetrmnl.com/recipes/' . $item['id']) : null, - ]; - }) + ->map(fn (array $item) => $this->mapRecipe($item)) ->toArray(); } + + /** + * @param array $item + * @return array + */ + private function mapRecipe(array $item): array + { + return [ + 'id' => $item['id'] ?? null, + 'name' => $item['name'] ?? 'Untitled', + 'icon_url' => $item['icon_url'] ?? null, + 'screenshot_url' => $item['screenshot_url'] ?? null, + 'author_bio' => is_array($item['author_bio'] ?? null) + ? strip_tags($item['author_bio']['description'] ?? null) + : null, + 'stats' => [ + 'installs' => data_get($item, 'stats.installs'), + 'forks' => data_get($item, 'stats.forks'), + ], + 'detail_url' => isset($item['id']) ? ('https://usetrmnl.com/recipes/'.$item['id']) : null, + ]; + } }; ?>
@@ -188,7 +211,7 @@ class extends Component { icon="magnifying-glass" />
- Newest + Newest
@error('installation') @@ -197,7 +220,7 @@ class extends Component { @if(empty($recipes))
- + No recipes found Try a different search term
@@ -211,8 +234,8 @@ class extends Component { @if($thumb) {{ $recipe['name'] }} @else -
- +
+
@endif @@ -221,12 +244,12 @@ class extends Component {
{{ $recipe['name'] }} @if(data_get($recipe, 'stats.installs')) - Installs: {{ data_get($recipe, 'stats.installs') }} · Forks: {{ data_get($recipe, 'stats.forks') }} + Installs: {{ data_get($recipe, 'stats.installs') }} · Forks: {{ data_get($recipe, 'stats.forks') }} @endif
@if($recipe['detail_url']) - + @endif @@ -246,7 +269,7 @@ class extends Component { @endif - @if($recipe['id']) + @if($recipe['id'] && ($recipe['screenshot_url'] ?? null)) - @if($previewingRecipe && !empty($previewData)) -
- Preview {{ $previewData['name'] ?? 'Recipe' }} +
+
+ + Fetching recipe details...
+
-
- @if($previewData['screenshot_url']) +
+ @if($previewingRecipe && !empty($previewData)) +
+ Preview {{ $previewData['name'] ?? 'Recipe' }} +
+ +
Preview of {{ $previewData['name'] }}
- @elseif($previewData['icon_url']) -
- {{ $previewData['name'] }} icon - No preview image available -
- @else -
- - No preview available -
- @endif - @if($previewData['author_bio']) -
-
- Description - {{ $previewData['author_bio'] }} + @if($previewData['author_bio']) +
+
+ Description + {{ $previewData['author_bio'] }} +
-
- @endif - - @if(data_get($previewData, 'stats.installs')) -
-
- Statistics - - Installs: {{ data_get($previewData, 'stats.installs') }} · - Forks: {{ data_get($previewData, 'stats.forks') }} - -
-
- @endif - -
- @if($previewData['detail_url']) - - View on TRMNL - @endif - - - Install Recipe - - + + @if(data_get($previewData, 'stats.installs')) +
+
+ Statistics + + Installs: {{ data_get($previewData, 'stats.installs') }} · + Forks: {{ data_get($previewData, 'stats.forks') }} + +
+
+ @endif + +
+ @if($previewData['detail_url']) + + View on TRMNL + + @endif + + + Install Recipe + + +
-
- @endif + @endif +
- -@script - -@endscript diff --git a/tests/Feature/Livewire/Catalog/IndexTest.php b/tests/Feature/Livewire/Catalog/IndexTest.php index 22ab4b6..1b2efba 100644 --- a/tests/Feature/Livewire/Catalog/IndexTest.php +++ b/tests/Feature/Livewire/Catalog/IndexTest.php @@ -65,6 +65,46 @@ it('loads plugins from catalog URL', function (): void { $component->assertSee('testuser'); $component->assertSee('A test plugin'); $component->assertSee('MIT'); + $component->assertSee('Preview'); +}); + +it('hides preview button when screenshot_url is missing', function (): void { + // Clear cache first to ensure fresh data + Cache::forget('catalog_plugins'); + + // Mock the HTTP response for the catalog URL without screenshot_url + $catalogData = [ + 'test-plugin' => [ + 'name' => 'Test Plugin Without Screenshot', + 'author' => ['name' => 'Test Author', 'github' => 'testuser'], + 'author_bio' => [ + 'description' => 'A test plugin', + ], + 'license' => 'MIT', + 'trmnlp' => [ + 'zip_url' => 'https://example.com/plugin.zip', + ], + 'byos' => [ + 'byos_laravel' => [ + 'compatibility' => true, + ], + ], + 'logo_url' => 'https://example.com/logo.png', + 'screenshot_url' => null, + ], + ]; + + $yamlContent = Yaml::dump($catalogData); + + Http::fake([ + config('app.catalog_url') => Http::response($yamlContent, 200), + ]); + + Livewire::withoutLazyLoading(); + + Volt::test('catalog.index') + ->assertSee('Test Plugin Without Screenshot') + ->assertDontSeeHtml('variant="subtle" icon="eye"'); }); it('shows error when plugin not found', function (): void { @@ -114,3 +154,46 @@ it('shows error when zip_url is missing', function (): void { $component->assertHasErrors(); }); + +it('can preview a plugin', function (): void { + // Clear cache first to ensure fresh data + Cache::forget('catalog_plugins'); + + // Mock the HTTP response for the catalog URL + $catalogData = [ + 'test-plugin' => [ + 'name' => 'Test Plugin', + 'author' => ['name' => 'Test Author', 'github' => 'testuser'], + 'author_bio' => [ + 'description' => 'A test plugin description', + ], + 'license' => 'MIT', + 'trmnlp' => [ + 'zip_url' => 'https://example.com/plugin.zip', + ], + 'byos' => [ + 'byos_laravel' => [ + 'compatibility' => true, + ], + ], + 'logo_url' => 'https://example.com/logo.png', + 'screenshot_url' => 'https://example.com/screenshot.png', + ], + ]; + + $yamlContent = Yaml::dump($catalogData); + + Http::fake([ + config('app.catalog_url') => Http::response($yamlContent, 200), + ]); + + Livewire::withoutLazyLoading(); + + Volt::test('catalog.index') + ->assertSee('Test Plugin') + ->call('previewPlugin', 'test-plugin') + ->assertSet('previewingPlugin', 'test-plugin') + ->assertSet('previewData.name', 'Test Plugin') + ->assertSee('Preview Test Plugin') + ->assertSee('A test plugin description'); +}); diff --git a/tests/Feature/Volt/CatalogTrmnlTest.php b/tests/Feature/Volt/CatalogTrmnlTest.php index ba1b722..4c338df 100644 --- a/tests/Feature/Volt/CatalogTrmnlTest.php +++ b/tests/Feature/Volt/CatalogTrmnlTest.php @@ -28,9 +28,33 @@ it('loads newest TRMNL recipes on mount', function (): void { Volt::test('catalog.trmnl') ->assertSee('Weather Chum') ->assertSee('Install') + ->assertDontSeeHtml('variant="subtle" icon="eye"') ->assertSee('Installs: 10'); }); +it('shows preview button when screenshot_url is provided', function (): void { + Http::fake([ + 'usetrmnl.com/recipes.json*' => Http::response([ + 'data' => [ + [ + 'id' => 123, + 'name' => 'Weather Chum', + 'icon_url' => 'https://example.com/icon.png', + 'screenshot_url' => 'https://example.com/screenshot.png', + 'author_bio' => null, + 'stats' => ['installs' => 10, 'forks' => 2], + ], + ], + ], 200), + ]); + + Livewire::withoutLazyLoading(); + + Volt::test('catalog.trmnl') + ->assertSee('Weather Chum') + ->assertSee('Preview'); +}); + it('searches TRMNL recipes when search term is provided', function (): void { Http::fake([ // First call (mount -> newest) @@ -152,3 +176,41 @@ it('shows error when plugin installation fails', function (): void { ->call('installPlugin', '123') ->assertSee('Error installing plugin'); // This will fail because the zip content is invalid }); + +it('previews a recipe with async fetch', function (): void { + Http::fake([ + 'usetrmnl.com/recipes.json*' => Http::response([ + 'data' => [ + [ + 'id' => 123, + 'name' => 'Weather Chum', + 'icon_url' => 'https://example.com/icon.png', + 'screenshot_url' => 'https://example.com/old.png', + 'author_bio' => null, + 'stats' => ['installs' => 10, 'forks' => 2], + ], + ], + ], 200), + 'usetrmnl.com/recipes/123.json' => Http::response([ + 'data' => [ + 'id' => 123, + 'name' => 'Weather Chum Updated', + 'icon_url' => 'https://example.com/icon.png', + 'screenshot_url' => 'https://example.com/new.png', + 'author_bio' => ['description' => 'New bio'], + 'stats' => ['installs' => 11, 'forks' => 3], + ], + ], 200), + ]); + + Livewire::withoutLazyLoading(); + + Volt::test('catalog.trmnl') + ->assertSee('Weather Chum') + ->call('previewRecipe', '123') + ->assertSet('previewingRecipe', '123') + ->assertSet('previewData.name', 'Weather Chum Updated') + ->assertSet('previewData.screenshot_url', 'https://example.com/new.png') + ->assertSee('Preview Weather Chum Updated') + ->assertSee('New bio'); +}); From 3250bb0402730e3195d4415b6904b7371199651c Mon Sep 17 00:00:00 2001 From: Benjamin Nussbaum Date: Tue, 30 Dec 2025 10:28:41 +0100 Subject: [PATCH 009/104] fix: install loading spinner not shown after catalog search --- resources/views/livewire/catalog/index.blade.php | 2 +- resources/views/livewire/catalog/trmnl.blade.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/resources/views/livewire/catalog/index.blade.php b/resources/views/livewire/catalog/index.blade.php index 3a24b7e..7257ab0 100644 --- a/resources/views/livewire/catalog/index.blade.php +++ b/resources/views/livewire/catalog/index.blade.php @@ -161,7 +161,7 @@ class extends Component @enderror @foreach($catalogPlugins as $plugin) -
+
@if($plugin['logo_url']) {{ $plugin['name'] }} diff --git a/resources/views/livewire/catalog/trmnl.blade.php b/resources/views/livewire/catalog/trmnl.blade.php index dd97e0e..8efd6b5 100644 --- a/resources/views/livewire/catalog/trmnl.blade.php +++ b/resources/views/livewire/catalog/trmnl.blade.php @@ -227,7 +227,7 @@ class extends Component @else
@foreach($recipes as $recipe) -
+
@php($thumb = $recipe['icon_url'] ?? $recipe['screenshot_url']) From 7f97114f6e521355bafce523dd867fc761a2b644 Mon Sep 17 00:00:00 2001 From: Benjamin Nussbaum Date: Tue, 30 Dec 2025 10:52:54 +0100 Subject: [PATCH 010/104] feat: add trmnl catalog paginator --- .../views/livewire/catalog/trmnl.blade.php | 77 +++++++++++++++---- tests/Feature/Volt/CatalogTrmnlTest.php | 70 +++++++++++++++++ 2 files changed, 134 insertions(+), 13 deletions(-) diff --git a/resources/views/livewire/catalog/trmnl.blade.php b/resources/views/livewire/catalog/trmnl.blade.php index 8efd6b5..9ecad1a 100644 --- a/resources/views/livewire/catalog/trmnl.blade.php +++ b/resources/views/livewire/catalog/trmnl.blade.php @@ -13,6 +13,10 @@ class extends Component { public array $recipes = []; + public int $page = 1; + + public bool $hasMore = false; + public string $search = ''; public bool $isSearching = false; @@ -43,23 +47,36 @@ class extends Component private function loadNewest(): void { try { - $this->recipes = Cache::remember('trmnl_recipes_newest', 43200, function () { + $cacheKey = 'trmnl_recipes_newest_page_'.$this->page; + $response = Cache::remember($cacheKey, 43200, function () { $response = Http::timeout(10)->get('https://usetrmnl.com/recipes.json', [ 'sort-by' => 'newest', + 'page' => $this->page, ]); if (! $response->successful()) { throw new RuntimeException('Failed to fetch TRMNL recipes'); } - $json = $response->json(); - $data = $json['data'] ?? []; - - return $this->mapRecipes($data); + return $response->json(); }); + + $data = $response['data'] ?? []; + $mapped = $this->mapRecipes($data); + + if ($this->page === 1) { + $this->recipes = $mapped; + } else { + $this->recipes = array_merge($this->recipes, $mapped); + } + + $this->hasMore = ! empty($response['next_page_url']); } catch (Throwable $e) { Log::error('TRMNL catalog load error: '.$e->getMessage()); - $this->recipes = []; + if ($this->page === 1) { + $this->recipes = []; + } + $this->hasMore = false; } } @@ -67,32 +84,57 @@ class extends Component { $this->isSearching = true; try { - $cacheKey = 'trmnl_recipes_search_'.md5($term); - $this->recipes = Cache::remember($cacheKey, 300, function () use ($term) { + $cacheKey = 'trmnl_recipes_search_'.md5($term).'_page_'.$this->page; + $response = Cache::remember($cacheKey, 300, function () use ($term) { $response = Http::get('https://usetrmnl.com/recipes.json', [ 'search' => $term, 'sort-by' => 'newest', + 'page' => $this->page, ]); if (! $response->successful()) { throw new RuntimeException('Failed to search TRMNL recipes'); } - $json = $response->json(); - $data = $json['data'] ?? []; - - return $this->mapRecipes($data); + return $response->json(); }); + + $data = $response['data'] ?? []; + $mapped = $this->mapRecipes($data); + + if ($this->page === 1) { + $this->recipes = $mapped; + } else { + $this->recipes = array_merge($this->recipes, $mapped); + } + + $this->hasMore = ! empty($response['next_page_url']); } catch (Throwable $e) { Log::error('TRMNL catalog search error: '.$e->getMessage()); - $this->recipes = []; + if ($this->page === 1) { + $this->recipes = []; + } + $this->hasMore = false; } finally { $this->isSearching = false; } } + public function loadMore(): void + { + $this->page++; + + $term = mb_trim($this->search); + if ($term === '' || mb_strlen($term) < 2) { + $this->loadNewest(); + } else { + $this->searchRecipes($term); + } + } + public function updatedSearch(): void { + $this->page = 1; $term = mb_trim($this->search); if ($term === '') { $this->loadNewest(); @@ -286,6 +328,15 @@ class extends Component
@endforeach
+ + @if($hasMore) +
+ + Load next page + Loading... + +
+ @endif @endif diff --git a/tests/Feature/Volt/CatalogTrmnlTest.php b/tests/Feature/Volt/CatalogTrmnlTest.php index 4c338df..a80c63a 100644 --- a/tests/Feature/Volt/CatalogTrmnlTest.php +++ b/tests/Feature/Volt/CatalogTrmnlTest.php @@ -214,3 +214,73 @@ it('previews a recipe with async fetch', function (): void { ->assertSee('Preview Weather Chum Updated') ->assertSee('New bio'); }); + +it('supports pagination and loading more recipes', function (): void { + Http::fake([ + 'usetrmnl.com/recipes.json?sort-by=newest&page=1' => Http::response([ + 'data' => [ + [ + 'id' => 1, + 'name' => 'Recipe Page 1', + 'icon_url' => null, + 'screenshot_url' => null, + 'author_bio' => null, + 'stats' => ['installs' => 1, 'forks' => 0], + ], + ], + 'next_page_url' => '/recipes.json?page=2', + ], 200), + 'usetrmnl.com/recipes.json?sort-by=newest&page=2' => Http::response([ + 'data' => [ + [ + 'id' => 2, + 'name' => 'Recipe Page 2', + 'icon_url' => null, + 'screenshot_url' => null, + 'author_bio' => null, + 'stats' => ['installs' => 2, 'forks' => 0], + ], + ], + 'next_page_url' => null, + ], 200), + ]); + + Livewire::withoutLazyLoading(); + + Volt::test('catalog.trmnl') + ->assertSee('Recipe Page 1') + ->assertDontSee('Recipe Page 2') + ->assertSee('Load next page') + ->call('loadMore') + ->assertSee('Recipe Page 1') + ->assertSee('Recipe Page 2') + ->assertDontSee('Load next page'); +}); + +it('resets pagination when search term changes', function (): void { + Http::fake([ + 'usetrmnl.com/recipes.json?sort-by=newest&page=1' => Http::sequence() + ->push([ + 'data' => [['id' => 1, 'name' => 'Initial 1']], + 'next_page_url' => '/recipes.json?page=2', + ]) + ->push([ + 'data' => [['id' => 3, 'name' => 'Initial 1 Again']], + 'next_page_url' => null, + ]), + 'usetrmnl.com/recipes.json?search=weather&sort-by=newest&page=1' => Http::response([ + 'data' => [['id' => 2, 'name' => 'Weather Result']], + 'next_page_url' => null, + ]), + ]); + + Livewire::withoutLazyLoading(); + + Volt::test('catalog.trmnl') + ->assertSee('Initial 1') + ->call('loadMore') + ->set('search', 'weather') + ->assertSee('Weather Result') + ->assertDontSee('Initial 1') + ->assertSet('page', 1); +}); From 265972ac24930cfaebb05d1a85344d5e2437ade7 Mon Sep 17 00:00:00 2001 From: Benjamin Nussbaum Date: Tue, 30 Dec 2025 14:09:31 +0100 Subject: [PATCH 011/104] fix(#130): server error on faulty recipes --- app/Services/ImageGenerationService.php | 25 ++- .../views/default-screens/error.blade.php | 23 +++ routes/api.php | 24 ++- tests/Feature/Api/DeviceEndpointsTest.php | 161 ++++++++++++++++++ 4 files changed, 221 insertions(+), 12 deletions(-) create mode 100644 resources/views/default-screens/error.blade.php diff --git a/app/Services/ImageGenerationService.php b/app/Services/ImageGenerationService.php index cdfc9d2..fcd5f12 100644 --- a/app/Services/ImageGenerationService.php +++ b/app/Services/ImageGenerationService.php @@ -311,7 +311,7 @@ class ImageGenerationService public static function getDeviceSpecificDefaultImage(Device $device, string $imageType): ?string { // Validate image type - if (! in_array($imageType, ['setup-logo', 'sleep'])) { + if (! in_array($imageType, ['setup-logo', 'sleep', 'error'])) { return null; } @@ -345,10 +345,10 @@ class ImageGenerationService /** * Generate a default screen image from Blade template */ - public static function generateDefaultScreenImage(Device $device, string $imageType): string + public static function generateDefaultScreenImage(Device $device, string $imageType, ?string $pluginName = null): string { // Validate image type - if (! in_array($imageType, ['setup-logo', 'sleep'])) { + if (! in_array($imageType, ['setup-logo', 'sleep', 'error'])) { throw new InvalidArgumentException("Invalid image type: {$imageType}"); } @@ -365,7 +365,7 @@ class ImageGenerationService $outputPath = Storage::disk('public')->path('/images/generated/'.$uuid.'.'.$fileExtension); // Generate HTML from Blade template - $html = self::generateDefaultScreenHtml($device, $imageType); + $html = self::generateDefaultScreenHtml($device, $imageType, $pluginName); // Create custom Browsershot instance if using AWS Lambda $browsershotInstance = null; @@ -445,12 +445,13 @@ class ImageGenerationService /** * Generate HTML from Blade template for default screens */ - private static function generateDefaultScreenHtml(Device $device, string $imageType): string + private static function generateDefaultScreenHtml(Device $device, string $imageType, ?string $pluginName = null): string { // Map image type to template name $templateName = match ($imageType) { 'setup-logo' => 'default-screens.setup', 'sleep' => 'default-screens.sleep', + 'error' => 'default-screens.error', default => throw new InvalidArgumentException("Invalid image type: {$imageType}") }; @@ -461,14 +462,22 @@ class ImageGenerationService $scaleLevel = $device->scaleLevel(); $darkMode = $imageType === 'sleep'; // Sleep mode uses dark mode, setup uses light mode - // Render the Blade template - return view($templateName, [ + // Build view data + $viewData = [ 'noBleed' => false, 'darkMode' => $darkMode, 'deviceVariant' => $deviceVariant, 'deviceOrientation' => $deviceOrientation, 'colorDepth' => $colorDepth, 'scaleLevel' => $scaleLevel, - ])->render(); + ]; + + // Add plugin name for error screens + if ($imageType === 'error' && $pluginName !== null) { + $viewData['pluginName'] = $pluginName; + } + + // Render the Blade template + return view($templateName, $viewData)->render(); } } diff --git a/resources/views/default-screens/error.blade.php b/resources/views/default-screens/error.blade.php new file mode 100644 index 0000000..be8063a --- /dev/null +++ b/resources/views/default-screens/error.blade.php @@ -0,0 +1,23 @@ +@props([ + 'noBleed' => false, + 'darkMode' => false, + 'deviceVariant' => 'og', + 'deviceOrientation' => null, + 'colorDepth' => '1bit', + 'scaleLevel' => null, + 'pluginName' => 'Recipe', +]) + + + + + + Error on {{ $pluginName }} + Unable to render content. Please check server logs. + + + + + diff --git a/routes/api.php b/routes/api.php index d1dbcac..b1d08b4 100644 --- a/routes/api.php +++ b/routes/api.php @@ -95,9 +95,16 @@ Route::get('/display', function (Request $request) { // Check and update stale data if needed if ($plugin->isDataStale() || $plugin->current_image === null) { $plugin->updateDataPayload(); - $markup = $plugin->render(device: $device); + try { + $markup = $plugin->render(device: $device); - GenerateScreenJob::dispatchSync($device->id, $plugin->id, $markup); + GenerateScreenJob::dispatchSync($device->id, $plugin->id, $markup); + } catch (Exception $e) { + Log::error("Failed to render plugin {$plugin->id} ({$plugin->name}): ".$e->getMessage()); + // Generate error display + $errorImageUuid = ImageGenerationService::generateDefaultScreenImage($device, 'error', $plugin->name); + $device->update(['current_screen_image' => $errorImageUuid]); + } } $plugin->refresh(); @@ -120,8 +127,17 @@ Route::get('/display', function (Request $request) { } } - $markup = $playlistItem->render(device: $device); - GenerateScreenJob::dispatchSync($device->id, null, $markup); + try { + $markup = $playlistItem->render(device: $device); + GenerateScreenJob::dispatchSync($device->id, null, $markup); + } catch (Exception $e) { + Log::error("Failed to render mashup playlist item {$playlistItem->id}: ".$e->getMessage()); + // For mashups, show error for the first plugin or a generic error + $firstPlugin = $plugins->first(); + $pluginName = $firstPlugin ? $firstPlugin->name : 'Recipe'; + $errorImageUuid = ImageGenerationService::generateDefaultScreenImage($device, 'error', $pluginName); + $device->update(['current_screen_image' => $errorImageUuid]); + } $device->refresh(); diff --git a/tests/Feature/Api/DeviceEndpointsTest.php b/tests/Feature/Api/DeviceEndpointsTest.php index aff6758..2925a5e 100644 --- a/tests/Feature/Api/DeviceEndpointsTest.php +++ b/tests/Feature/Api/DeviceEndpointsTest.php @@ -7,6 +7,7 @@ use App\Models\Playlist; use App\Models\PlaylistItem; use App\Models\Plugin; use App\Models\User; +use App\Services\ImageGenerationService; use Bnussbau\TrmnlPipeline\TrmnlPipeline; use Illuminate\Support\Facades\Queue; use Illuminate\Support\Facades\Storage; @@ -1023,3 +1024,163 @@ test('screens endpoint matches MAC address case-insensitively', function (): voi $response->assertOk(); Queue::assertPushed(GenerateScreenJob::class); }); + +test('display endpoint handles plugin rendering errors gracefully', function (): void { + TrmnlPipeline::fake(); + + $device = Device::factory()->create([ + 'mac_address' => '00:11:22:33:44:55', + 'api_key' => 'test-api-key', + 'proxy_cloud' => false, + ]); + + // Create a plugin with Blade markup that will cause an exception when accessing data[0] + // when data is not an array or doesn't have index 0 + $plugin = Plugin::factory()->create([ + 'name' => 'Broken Recipe', + 'data_strategy' => 'polling', + 'polling_url' => null, + 'data_stale_minutes' => 1, + 'markup_language' => 'blade', // Use Blade which will throw exception on invalid array access + 'render_markup' => '
{{ $data[0]["invalid"] }}
', // This will fail if data[0] doesn't exist + 'data_payload' => ['error' => 'Failed to fetch data'], // Not a list, so data[0] will fail + 'data_payload_updated_at' => now()->subMinutes(2), // Make it stale + 'current_image' => null, + ]); + + $playlist = Playlist::factory()->create([ + 'device_id' => $device->id, + 'name' => 'test_playlist', + 'is_active' => true, + 'weekdays' => null, + 'active_from' => null, + 'active_until' => null, + ]); + + PlaylistItem::factory()->create([ + 'playlist_id' => $playlist->id, + 'plugin_id' => $plugin->id, + 'order' => 1, + 'is_active' => true, + 'last_displayed_at' => null, + ]); + + $response = $this->withHeaders([ + 'id' => $device->mac_address, + 'access-token' => $device->api_key, + 'rssi' => -70, + 'battery_voltage' => 3.8, + 'fw-version' => '1.0.0', + ])->get('/api/display'); + + $response->assertOk(); + + // Verify error screen was generated and set on device + $device->refresh(); + expect($device->current_screen_image)->not->toBeNull(); + + // Verify the error image exists + $errorImagePath = Storage::disk('public')->path("images/generated/{$device->current_screen_image}.png"); + // The TrmnlPipeline is faked, so we just verify the UUID was set + expect($device->current_screen_image)->toBeString(); +}); + +test('display endpoint handles mashup rendering errors gracefully', function (): void { + TrmnlPipeline::fake(); + + $device = Device::factory()->create([ + 'mac_address' => '00:11:22:33:44:55', + 'api_key' => 'test-api-key', + 'proxy_cloud' => false, + ]); + + // Create plugins for mashup, one with invalid markup + $plugin1 = Plugin::factory()->create([ + 'name' => 'Working Plugin', + 'data_strategy' => 'polling', + 'polling_url' => null, + 'data_stale_minutes' => 1, + 'render_markup_view' => 'trmnl', + 'data_payload_updated_at' => now()->subMinutes(2), + 'current_image' => null, + ]); + + $plugin2 = Plugin::factory()->create([ + 'name' => 'Broken Plugin', + 'data_strategy' => 'polling', + 'polling_url' => null, + 'data_stale_minutes' => 1, + 'markup_language' => 'blade', // Use Blade which will throw exception on invalid array access + 'render_markup' => '
{{ $data[0]["invalid"] }}
', // This will fail + 'data_payload' => ['error' => 'Failed to fetch data'], + 'data_payload_updated_at' => now()->subMinutes(2), + 'current_image' => null, + ]); + + $playlist = Playlist::factory()->create([ + 'device_id' => $device->id, + 'name' => 'test_playlist', + 'is_active' => true, + 'weekdays' => null, + 'active_from' => null, + 'active_until' => null, + ]); + + // Create mashup playlist item + $playlistItem = PlaylistItem::createMashup( + $playlist, + '1Lx1R', + [$plugin1->id, $plugin2->id], + 'Test Mashup', + 1 + ); + + $response = $this->withHeaders([ + 'id' => $device->mac_address, + 'access-token' => $device->api_key, + 'rssi' => -70, + 'battery_voltage' => 3.8, + 'fw-version' => '1.0.0', + ])->get('/api/display'); + + $response->assertOk(); + + // Verify error screen was generated and set on device + $device->refresh(); + expect($device->current_screen_image)->not->toBeNull(); + + // Verify the error image UUID was set + expect($device->current_screen_image)->toBeString(); +}); + +test('generateDefaultScreenImage creates error screen with plugin name', function (): void { + TrmnlPipeline::fake(); + Storage::fake('public'); + Storage::disk('public')->makeDirectory('/images/generated'); + + $device = Device::factory()->create(); + + $errorUuid = ImageGenerationService::generateDefaultScreenImage($device, 'error', 'Test Recipe Name'); + + expect($errorUuid)->not->toBeEmpty(); + + // Verify the error image path would be created + $errorPath = "images/generated/{$errorUuid}.png"; + // Since TrmnlPipeline is faked, we just verify the UUID was generated + expect($errorUuid)->toBeString(); +}); + +test('generateDefaultScreenImage throws exception for invalid error image type', function (): void { + $device = Device::factory()->create(); + + expect(fn (): string => ImageGenerationService::generateDefaultScreenImage($device, 'invalid-error-type')) + ->toThrow(InvalidArgumentException::class); +}); + +test('getDeviceSpecificDefaultImage returns null for error type when no device-specific image exists', function (): void { + $device = new Device(); + $device->deviceModel = null; + + $result = ImageGenerationService::getDeviceSpecificDefaultImage($device, 'error'); + expect($result)->toBeNull(); +}); From 4451361f1547e577e2265323ffb238f338afc43b Mon Sep 17 00:00:00 2001 From: Benjamin Nussbaum Date: Wed, 31 Dec 2025 10:20:03 +0100 Subject: [PATCH 012/104] chore: update dependencies --- composer.lock | 164 +++++++++++++++++++++++++------------------------- 1 file changed, 82 insertions(+), 82 deletions(-) diff --git a/composer.lock b/composer.lock index 1b578bf..199fa86 100644 --- a/composer.lock +++ b/composer.lock @@ -62,16 +62,16 @@ }, { "name": "aws/aws-sdk-php", - "version": "3.369.4", + "version": "3.369.5", "source": { "type": "git", "url": "https://github.com/aws/aws-sdk-php.git", - "reference": "2aa1ef195e90140d733382e4341732ce113024f5" + "reference": "7cb482768899d510e8bcb3e9ef685d2ed0afcbfe" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/2aa1ef195e90140d733382e4341732ce113024f5", - "reference": "2aa1ef195e90140d733382e4341732ce113024f5", + "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/7cb482768899d510e8bcb3e9ef685d2ed0afcbfe", + "reference": "7cb482768899d510e8bcb3e9ef685d2ed0afcbfe", "shasum": "" }, "require": { @@ -153,9 +153,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.369.4" + "source": "https://github.com/aws/aws-sdk-php/tree/3.369.5" }, - "time": "2025-12-29T19:07:47+00:00" + "time": "2025-12-30T19:07:16+00:00" }, { "name": "bnussbau/laravel-trmnl-blade", @@ -3142,16 +3142,16 @@ }, { "name": "monolog/monolog", - "version": "3.9.0", + "version": "3.10.0", "source": { "type": "git", "url": "https://github.com/Seldaek/monolog.git", - "reference": "10d85740180ecba7896c87e06a166e0c95a0e3b6" + "reference": "b321dd6749f0bf7189444158a3ce785cc16d69b0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Seldaek/monolog/zipball/10d85740180ecba7896c87e06a166e0c95a0e3b6", - "reference": "10d85740180ecba7896c87e06a166e0c95a0e3b6", + "url": "https://api.github.com/repos/Seldaek/monolog/zipball/b321dd6749f0bf7189444158a3ce785cc16d69b0", + "reference": "b321dd6749f0bf7189444158a3ce785cc16d69b0", "shasum": "" }, "require": { @@ -3169,7 +3169,7 @@ "graylog2/gelf-php": "^1.4.2 || ^2.0", "guzzlehttp/guzzle": "^7.4.5", "guzzlehttp/psr7": "^2.2", - "mongodb/mongodb": "^1.8", + "mongodb/mongodb": "^1.8 || ^2.0", "php-amqplib/php-amqplib": "~2.4 || ^3", "php-console/php-console": "^3.1.8", "phpstan/phpstan": "^2", @@ -3229,7 +3229,7 @@ ], "support": { "issues": "https://github.com/Seldaek/monolog/issues", - "source": "https://github.com/Seldaek/monolog/tree/3.9.0" + "source": "https://github.com/Seldaek/monolog/tree/3.10.0" }, "funding": [ { @@ -3241,7 +3241,7 @@ "type": "tidelift" } ], - "time": "2025-03-24T10:02:05+00:00" + "time": "2026-01-02T08:56:05+00:00" }, { "name": "mtdowling/jmespath.php", @@ -5026,16 +5026,16 @@ }, { "name": "symfony/console", - "version": "v7.4.1", + "version": "v7.4.3", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "6d9f0fbf2ec2e9785880096e3abd0ca0c88b506e" + "reference": "732a9ca6cd9dfd940c639062d5edbde2f6727fb6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/6d9f0fbf2ec2e9785880096e3abd0ca0c88b506e", - "reference": "6d9f0fbf2ec2e9785880096e3abd0ca0c88b506e", + "url": "https://api.github.com/repos/symfony/console/zipball/732a9ca6cd9dfd940c639062d5edbde2f6727fb6", + "reference": "732a9ca6cd9dfd940c639062d5edbde2f6727fb6", "shasum": "" }, "require": { @@ -5100,7 +5100,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v7.4.1" + "source": "https://github.com/symfony/console/tree/v7.4.3" }, "funding": [ { @@ -5120,7 +5120,7 @@ "type": "tidelift" } ], - "time": "2025-12-05T15:23:39+00:00" + "time": "2025-12-23T14:50:43+00:00" }, { "name": "symfony/css-selector", @@ -5573,16 +5573,16 @@ }, { "name": "symfony/finder", - "version": "v7.4.0", + "version": "v7.4.3", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "340b9ed7320570f319028a2cbec46d40535e94bd" + "reference": "fffe05569336549b20a1be64250b40516d6e8d06" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/340b9ed7320570f319028a2cbec46d40535e94bd", - "reference": "340b9ed7320570f319028a2cbec46d40535e94bd", + "url": "https://api.github.com/repos/symfony/finder/zipball/fffe05569336549b20a1be64250b40516d6e8d06", + "reference": "fffe05569336549b20a1be64250b40516d6e8d06", "shasum": "" }, "require": { @@ -5617,7 +5617,7 @@ "description": "Finds files and directories via an intuitive fluent interface", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/finder/tree/v7.4.0" + "source": "https://github.com/symfony/finder/tree/v7.4.3" }, "funding": [ { @@ -5637,20 +5637,20 @@ "type": "tidelift" } ], - "time": "2025-11-05T05:42:40+00:00" + "time": "2025-12-23T14:50:43+00:00" }, { "name": "symfony/http-foundation", - "version": "v7.4.1", + "version": "v7.4.3", "source": { "type": "git", "url": "https://github.com/symfony/http-foundation.git", - "reference": "bd1af1e425811d6f077db240c3a588bdb405cd27" + "reference": "a70c745d4cea48dbd609f4075e5f5cbce453bd52" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-foundation/zipball/bd1af1e425811d6f077db240c3a588bdb405cd27", - "reference": "bd1af1e425811d6f077db240c3a588bdb405cd27", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/a70c745d4cea48dbd609f4075e5f5cbce453bd52", + "reference": "a70c745d4cea48dbd609f4075e5f5cbce453bd52", "shasum": "" }, "require": { @@ -5699,7 +5699,7 @@ "description": "Defines an object-oriented layer for the HTTP specification", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/http-foundation/tree/v7.4.1" + "source": "https://github.com/symfony/http-foundation/tree/v7.4.3" }, "funding": [ { @@ -5719,20 +5719,20 @@ "type": "tidelift" } ], - "time": "2025-12-07T11:13:10+00:00" + "time": "2025-12-23T14:23:49+00:00" }, { "name": "symfony/http-kernel", - "version": "v7.4.2", + "version": "v7.4.3", "source": { "type": "git", "url": "https://github.com/symfony/http-kernel.git", - "reference": "f6e6f0a5fa8763f75a504b930163785fb6dd055f" + "reference": "885211d4bed3f857b8c964011923528a55702aa5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-kernel/zipball/f6e6f0a5fa8763f75a504b930163785fb6dd055f", - "reference": "f6e6f0a5fa8763f75a504b930163785fb6dd055f", + "url": "https://api.github.com/repos/symfony/http-kernel/zipball/885211d4bed3f857b8c964011923528a55702aa5", + "reference": "885211d4bed3f857b8c964011923528a55702aa5", "shasum": "" }, "require": { @@ -5818,7 +5818,7 @@ "description": "Provides a structured process for converting a Request into a Response", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/http-kernel/tree/v7.4.2" + "source": "https://github.com/symfony/http-kernel/tree/v7.4.3" }, "funding": [ { @@ -5838,20 +5838,20 @@ "type": "tidelift" } ], - "time": "2025-12-08T07:43:37+00:00" + "time": "2025-12-31T08:43:57+00:00" }, { "name": "symfony/mailer", - "version": "v7.4.0", + "version": "v7.4.3", "source": { "type": "git", "url": "https://github.com/symfony/mailer.git", - "reference": "a3d9eea8cfa467ece41f0f54ba28185d74bd53fd" + "reference": "e472d35e230108231ccb7f51eb6b2100cac02ee4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/mailer/zipball/a3d9eea8cfa467ece41f0f54ba28185d74bd53fd", - "reference": "a3d9eea8cfa467ece41f0f54ba28185d74bd53fd", + "url": "https://api.github.com/repos/symfony/mailer/zipball/e472d35e230108231ccb7f51eb6b2100cac02ee4", + "reference": "e472d35e230108231ccb7f51eb6b2100cac02ee4", "shasum": "" }, "require": { @@ -5902,7 +5902,7 @@ "description": "Helps sending emails", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/mailer/tree/v7.4.0" + "source": "https://github.com/symfony/mailer/tree/v7.4.3" }, "funding": [ { @@ -5922,7 +5922,7 @@ "type": "tidelift" } ], - "time": "2025-11-21T15:26:00+00:00" + "time": "2025-12-16T08:02:06+00:00" }, { "name": "symfony/mime", @@ -6844,16 +6844,16 @@ }, { "name": "symfony/process", - "version": "v7.4.0", + "version": "v7.4.3", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "7ca8dc2d0dcf4882658313aba8be5d9fd01026c8" + "reference": "2f8e1a6cdf590ca63715da4d3a7a3327404a523f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/7ca8dc2d0dcf4882658313aba8be5d9fd01026c8", - "reference": "7ca8dc2d0dcf4882658313aba8be5d9fd01026c8", + "url": "https://api.github.com/repos/symfony/process/zipball/2f8e1a6cdf590ca63715da4d3a7a3327404a523f", + "reference": "2f8e1a6cdf590ca63715da4d3a7a3327404a523f", "shasum": "" }, "require": { @@ -6885,7 +6885,7 @@ "description": "Executes commands in sub-processes", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/process/tree/v7.4.0" + "source": "https://github.com/symfony/process/tree/v7.4.3" }, "funding": [ { @@ -6905,20 +6905,20 @@ "type": "tidelift" } ], - "time": "2025-10-16T11:21:06+00:00" + "time": "2025-12-19T10:00:43+00:00" }, { "name": "symfony/routing", - "version": "v7.4.0", + "version": "v7.4.3", "source": { "type": "git", "url": "https://github.com/symfony/routing.git", - "reference": "4720254cb2644a0b876233d258a32bf017330db7" + "reference": "5d3fd7adf8896c2fdb54e2f0f35b1bcbd9e45090" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/routing/zipball/4720254cb2644a0b876233d258a32bf017330db7", - "reference": "4720254cb2644a0b876233d258a32bf017330db7", + "url": "https://api.github.com/repos/symfony/routing/zipball/5d3fd7adf8896c2fdb54e2f0f35b1bcbd9e45090", + "reference": "5d3fd7adf8896c2fdb54e2f0f35b1bcbd9e45090", "shasum": "" }, "require": { @@ -6970,7 +6970,7 @@ "url" ], "support": { - "source": "https://github.com/symfony/routing/tree/v7.4.0" + "source": "https://github.com/symfony/routing/tree/v7.4.3" }, "funding": [ { @@ -6990,7 +6990,7 @@ "type": "tidelift" } ], - "time": "2025-11-27T13:27:24+00:00" + "time": "2025-12-19T10:00:43+00:00" }, { "name": "symfony/service-contracts", @@ -7171,16 +7171,16 @@ }, { "name": "symfony/translation", - "version": "v8.0.1", + "version": "v8.0.3", "source": { "type": "git", "url": "https://github.com/symfony/translation.git", - "reference": "770e3b8b0ba8360958abedcabacd4203467333ca" + "reference": "60a8f11f0e15c48f2cc47c4da53873bb5b62135d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/translation/zipball/770e3b8b0ba8360958abedcabacd4203467333ca", - "reference": "770e3b8b0ba8360958abedcabacd4203467333ca", + "url": "https://api.github.com/repos/symfony/translation/zipball/60a8f11f0e15c48f2cc47c4da53873bb5b62135d", + "reference": "60a8f11f0e15c48f2cc47c4da53873bb5b62135d", "shasum": "" }, "require": { @@ -7240,7 +7240,7 @@ "description": "Provides tools to internationalize your application", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/translation/tree/v8.0.1" + "source": "https://github.com/symfony/translation/tree/v8.0.3" }, "funding": [ { @@ -7260,7 +7260,7 @@ "type": "tidelift" } ], - "time": "2025-12-01T09:13:36+00:00" + "time": "2025-12-21T10:59:45+00:00" }, { "name": "symfony/translation-contracts", @@ -7424,16 +7424,16 @@ }, { "name": "symfony/var-dumper", - "version": "v7.4.0", + "version": "v7.4.3", "source": { "type": "git", "url": "https://github.com/symfony/var-dumper.git", - "reference": "41fd6c4ae28c38b294b42af6db61446594a0dece" + "reference": "7e99bebcb3f90d8721890f2963463280848cba92" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-dumper/zipball/41fd6c4ae28c38b294b42af6db61446594a0dece", - "reference": "41fd6c4ae28c38b294b42af6db61446594a0dece", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/7e99bebcb3f90d8721890f2963463280848cba92", + "reference": "7e99bebcb3f90d8721890f2963463280848cba92", "shasum": "" }, "require": { @@ -7487,7 +7487,7 @@ "dump" ], "support": { - "source": "https://github.com/symfony/var-dumper/tree/v7.4.0" + "source": "https://github.com/symfony/var-dumper/tree/v7.4.3" }, "funding": [ { @@ -7507,7 +7507,7 @@ "type": "tidelift" } ], - "time": "2025-10-27T20:36:44+00:00" + "time": "2025-12-18T07:04:31+00:00" }, { "name": "symfony/var-exporter", @@ -9198,16 +9198,16 @@ }, { "name": "pestphp/pest", - "version": "v4.2.0", + "version": "v4.3.0", "source": { "type": "git", "url": "https://github.com/pestphp/pest.git", - "reference": "7c43c1c5834435ed9f4ad635e9cb1f0064f876bd" + "reference": "e86bec3e68f1874c112ca782fb9db1333f3fe7ab" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/pestphp/pest/zipball/7c43c1c5834435ed9f4ad635e9cb1f0064f876bd", - "reference": "7c43c1c5834435ed9f4ad635e9cb1f0064f876bd", + "url": "https://api.github.com/repos/pestphp/pest/zipball/e86bec3e68f1874c112ca782fb9db1333f3fe7ab", + "reference": "e86bec3e68f1874c112ca782fb9db1333f3fe7ab", "shasum": "" }, "require": { @@ -9219,12 +9219,12 @@ "pestphp/pest-plugin-mutate": "^4.0.1", "pestphp/pest-plugin-profanity": "^4.2.1", "php": "^8.3.0", - "phpunit/phpunit": "^12.5.3", + "phpunit/phpunit": "^12.5.4", "symfony/process": "^7.4.0|^8.0.0" }, "conflict": { "filp/whoops": "<2.18.3", - "phpunit/phpunit": ">12.5.3", + "phpunit/phpunit": ">12.5.4", "sebastian/exporter": "<7.0.0", "webmozart/assert": "<1.11.0" }, @@ -9232,7 +9232,7 @@ "pestphp/pest-dev-tools": "^4.0.0", "pestphp/pest-plugin-browser": "^4.1.1", "pestphp/pest-plugin-type-coverage": "^4.0.3", - "psy/psysh": "^0.12.17" + "psy/psysh": "^0.12.18" }, "bin": [ "bin/pest" @@ -9298,7 +9298,7 @@ ], "support": { "issues": "https://github.com/pestphp/pest/issues", - "source": "https://github.com/pestphp/pest/tree/v4.2.0" + "source": "https://github.com/pestphp/pest/tree/v4.3.0" }, "funding": [ { @@ -9310,7 +9310,7 @@ "type": "github" } ], - "time": "2025-12-15T11:49:28+00:00" + "time": "2025-12-30T19:48:33+00:00" }, { "name": "pestphp/pest-plugin", @@ -10456,16 +10456,16 @@ }, { "name": "phpunit/phpunit", - "version": "12.5.3", + "version": "12.5.4", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "6dc2e076d09960efbb0c1272aa9bc156fc80955e" + "reference": "4ba0e923f9d3fc655de22f9547c01d15a41fc93a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/6dc2e076d09960efbb0c1272aa9bc156fc80955e", - "reference": "6dc2e076d09960efbb0c1272aa9bc156fc80955e", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/4ba0e923f9d3fc655de22f9547c01d15a41fc93a", + "reference": "4ba0e923f9d3fc655de22f9547c01d15a41fc93a", "shasum": "" }, "require": { @@ -10533,7 +10533,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/12.5.3" + "source": "https://github.com/sebastianbergmann/phpunit/tree/12.5.4" }, "funding": [ { @@ -10557,7 +10557,7 @@ "type": "tidelift" } ], - "time": "2025-12-11T08:52:59+00:00" + "time": "2025-12-15T06:05:34+00:00" }, { "name": "rector/rector", From 838b4fd33b223be1e6991f934a1b5e36dfbd38eb Mon Sep 17 00:00:00 2001 From: Benjamin Nussbaum Date: Fri, 2 Jan 2026 22:20:42 +0100 Subject: [PATCH 013/104] feat: bump to Design Framework 2.1 --- composer.json | 3 ++- composer.lock | 26 +++++++++++++------------- 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/composer.json b/composer.json index 2281415..f801679 100644 --- a/composer.json +++ b/composer.json @@ -6,6 +6,7 @@ "keywords": [ "trmnl", "trmnl-server", + "trmnl-byos", "laravel" ], "license": "MIT", @@ -14,7 +15,7 @@ "ext-imagick": "*", "ext-simplexml": "*", "ext-zip": "*", - "bnussbau/laravel-trmnl-blade": "2.0.*", + "bnussbau/laravel-trmnl-blade": "2.1.*", "bnussbau/trmnl-pipeline-php": "^0.6.0", "keepsuit/laravel-liquid": "^0.5.2", "laravel/framework": "^12.1", diff --git a/composer.lock b/composer.lock index 199fa86..b9e0495 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": "3e4c22c016c04e49512b5fcd20983baa", + "content-hash": "4d958d48655a5ad9e3de6b4a9fb52b0a", "packages": [ { "name": "aws/aws-crt-php", @@ -62,16 +62,16 @@ }, { "name": "aws/aws-sdk-php", - "version": "3.369.5", + "version": "3.369.6", "source": { "type": "git", "url": "https://github.com/aws/aws-sdk-php.git", - "reference": "7cb482768899d510e8bcb3e9ef685d2ed0afcbfe" + "reference": "b1e1846a4b6593b6916764d86fc0890a31727370" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/7cb482768899d510e8bcb3e9ef685d2ed0afcbfe", - "reference": "7cb482768899d510e8bcb3e9ef685d2ed0afcbfe", + "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/b1e1846a4b6593b6916764d86fc0890a31727370", + "reference": "b1e1846a4b6593b6916764d86fc0890a31727370", "shasum": "" }, "require": { @@ -153,22 +153,22 @@ "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.369.5" + "source": "https://github.com/aws/aws-sdk-php/tree/3.369.6" }, - "time": "2025-12-30T19:07:16+00:00" + "time": "2026-01-02T19:09:23+00:00" }, { "name": "bnussbau/laravel-trmnl-blade", - "version": "2.0.1", + "version": "2.1.0", "source": { "type": "git", "url": "https://github.com/bnussbau/laravel-trmnl-blade.git", - "reference": "59343cfa9c41c7c7f9285b366584cde92bf1294e" + "reference": "1e1cabfead00118d7a80c86ac6108aece2989bc7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/bnussbau/laravel-trmnl-blade/zipball/59343cfa9c41c7c7f9285b366584cde92bf1294e", - "reference": "59343cfa9c41c7c7f9285b366584cde92bf1294e", + "url": "https://api.github.com/repos/bnussbau/laravel-trmnl-blade/zipball/1e1cabfead00118d7a80c86ac6108aece2989bc7", + "reference": "1e1cabfead00118d7a80c86ac6108aece2989bc7", "shasum": "" }, "require": { @@ -223,7 +223,7 @@ ], "support": { "issues": "https://github.com/bnussbau/laravel-trmnl-blade/issues", - "source": "https://github.com/bnussbau/laravel-trmnl-blade/tree/2.0.1" + "source": "https://github.com/bnussbau/laravel-trmnl-blade/tree/2.1.0" }, "funding": [ { @@ -239,7 +239,7 @@ "type": "github" } ], - "time": "2025-09-22T12:12:00+00:00" + "time": "2026-01-02T20:38:51+00:00" }, { "name": "bnussbau/trmnl-pipeline-php", From 9019561bb3b7057e8bbffd4979255e7e8db4ead7 Mon Sep 17 00:00:00 2001 From: jerremyng Date: Sat, 3 Jan 2026 17:25:37 +0000 Subject: [PATCH 014/104] add zip dependencies to dev-container dockerfiles --- .devcontainer/cli/Dockerfile | 5 +++-- .devcontainer/fpm/Dockerfile | 5 +++-- package-lock.json | 13 ++----------- 3 files changed, 8 insertions(+), 15 deletions(-) diff --git a/.devcontainer/cli/Dockerfile b/.devcontainer/cli/Dockerfile index 0317097..ab13330 100644 --- a/.devcontainer/cli/Dockerfile +++ b/.devcontainer/cli/Dockerfile @@ -9,7 +9,8 @@ RUN apk add --no-cache composer # Add Chromium and Image Magick for puppeteer. RUN apk add --no-cache \ imagemagick-dev \ - chromium + chromium \ + libzip-dev ENV PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium ENV PUPPETEER_DOCKER=1 @@ -19,7 +20,7 @@ RUN chmod 777 /usr/src/php/ext/imagick RUN curl -fsSL https://github.com/Imagick/imagick/archive/refs/tags/3.8.0.tar.gz | tar xvz -C "/usr/src/php/ext/imagick" --strip 1 # Install PHP extensions -RUN docker-php-ext-install imagick +RUN docker-php-ext-install imagick zip # Composer uses its php binary, but we want it to use the container's one RUN rm -f /usr/bin/php84 diff --git a/.devcontainer/fpm/Dockerfile b/.devcontainer/fpm/Dockerfile index 8c585c8..3e658b6 100644 --- a/.devcontainer/fpm/Dockerfile +++ b/.devcontainer/fpm/Dockerfile @@ -14,7 +14,8 @@ RUN apk add --no-cache \ nodejs \ npm \ imagemagick-dev \ - chromium + chromium \ + libzip-dev ENV PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium ENV PUPPETEER_DOCKER=1 @@ -24,7 +25,7 @@ RUN chmod 777 /usr/src/php/ext/imagick RUN curl -fsSL https://github.com/Imagick/imagick/archive/refs/tags/3.8.0.tar.gz | tar xvz -C "/usr/src/php/ext/imagick" --strip 1 # Install PHP extensions -RUN docker-php-ext-install imagick +RUN docker-php-ext-install imagick zip RUN rm -f /usr/bin/php84 RUN ln -s /usr/local/bin/php /usr/bin/php84 diff --git a/package-lock.json b/package-lock.json index 8411d6a..e722432 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,5 +1,5 @@ { - "name": "laravel-trmnl-server", + "name": "laravel", "lockfileVersion": 3, "requires": true, "packages": { @@ -156,7 +156,6 @@ "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.11.3.tgz", "integrity": "sha512-9HBM2XnwDj7fnu0551HkGdrUrrqmYq/WC5iv6nbY2WdicXdGbhR/gfbZOH73Aqj4351alY1+aoG9rCNfiwS1RA==", "license": "MIT", - "peer": true, "dependencies": { "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.23.0", @@ -193,7 +192,6 @@ "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.5.2.tgz", "integrity": "sha512-FVqsPqtPWKVVL3dPSxy8wEF/ymIEuVzF1PK3VbUgrxXpJUSHQWWZz4JMToquRxnkw+36LTamCZG2iua2Ptq0fA==", "license": "MIT", - "peer": true, "dependencies": { "@marijn/find-cluster-break": "^1.0.0" } @@ -215,7 +213,6 @@ "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.38.8.tgz", "integrity": "sha512-XcE9fcnkHCbWkjeKyi0lllwXmBLtyYb5dt89dJyx23I9+LSh5vZDIuk7OLG4VM1lgrXZQcY6cxyZyk5WVPRv/A==", "license": "MIT", - "peer": true, "dependencies": { "@codemirror/state": "^6.5.0", "crelt": "^1.0.6", @@ -718,7 +715,6 @@ "resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.3.tgz", "integrity": "sha512-qXdH7UqTvGfdVBINrgKhDsVTJTxactNNxLk7+UMwZhU13lMHaOBlJe9Vqp907ya56Y3+ed2tlqzys7jDkTmW0g==", "license": "MIT", - "peer": true, "dependencies": { "@lezer/common": "^1.3.0" } @@ -1614,7 +1610,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -1898,8 +1893,7 @@ "version": "0.0.1521046", "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1521046.tgz", "integrity": "sha512-vhE6eymDQSKWUXwwA37NtTTVEzjtGVfDr3pRbsWEQ5onH/Snp2c+2xZHWJJawG/0hCCJLRGt4xVtEVUVILol4w==", - "license": "BSD-3-Clause", - "peer": true + "license": "BSD-3-Clause" }, "node_modules/dunder-proto": { "version": "1.0.1", @@ -2951,7 +2945,6 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -2978,7 +2971,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -3429,7 +3421,6 @@ "resolved": "https://registry.npmjs.org/vite/-/vite-7.2.6.tgz", "integrity": "sha512-tI2l/nFHC5rLh7+5+o7QjKjSR04ivXDF4jcgV0f/bTQ+OJiITy5S6gaynVsEM+7RqzufMnVbIon6Sr5x1SDYaQ==", "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", From 46e792bc6d79d3acc2bb62471aef5615153fe5ca Mon Sep 17 00:00:00 2001 From: jerremyng Date: Sun, 4 Jan 2026 08:15:09 +0000 Subject: [PATCH 015/104] add HTML rendering on config modal with tests Models/Plugin will now sanitize "description" and "help text" before loading. This allows HTML from these fields to be rendered safely. Sanitization is done using Purify library for completeness (new dependency). A test suite of simple xss attacks is also added. --- app/Models/Plugin.php | 24 ++ composer.json | 1 + composer.lock | 129 ++++++- resources/css/app.css | 4 + .../views/livewire/plugins/recipe.blade.php | 356 ++++++++++-------- tests/Unit/Models/PluginTest.php | 114 +++++- 6 files changed, 470 insertions(+), 158 deletions(-) diff --git a/app/Models/Plugin.php b/app/Models/Plugin.php index 9132d6c..6f5d88b 100644 --- a/app/Models/Plugin.php +++ b/app/Models/Plugin.php @@ -62,6 +62,11 @@ class Plugin extends Model $model->current_image = null; } }); + + // Sanitize configuration template on save + static::saving(function ($model): void { + $model->sanitizeTemplate(); + }); } public function user() @@ -69,6 +74,25 @@ class Plugin extends Model return $this->belongsTo(User::class); } + // sanitize configuration template descriptions and help texts (since they allow HTML rendering) + protected function sanitizeTemplate(): void + { + $template = $this->configuration_template; + + if (isset($template['custom_fields']) && is_array($template['custom_fields'])) { + foreach ($template['custom_fields'] as &$field) { + if (isset($field['description'])) { + $field['description'] = \Stevebauman\Purify\Facades\Purify::clean($field['description']); + } + if (isset($field['help_text'])) { + $field['help_text'] = \Stevebauman\Purify\Facades\Purify::clean($field['help_text']); + } + } + + $this->configuration_template = $template; + } + } + public function hasMissingRequiredConfigurationFields(): bool { if (! isset($this->configuration_template['custom_fields']) || empty($this->configuration_template['custom_fields'])) { diff --git a/composer.json b/composer.json index f801679..0ced4da 100644 --- a/composer.json +++ b/composer.json @@ -26,6 +26,7 @@ "livewire/volt": "^1.7", "om/icalparser": "^3.2", "spatie/browsershot": "^5.0", + "stevebauman/purify": "^6.3", "symfony/yaml": "^7.3", "wnx/sidecar-browsershot": "^2.6" }, diff --git a/composer.lock b/composer.lock index b9e0495..9767a0d 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": "4d958d48655a5ad9e3de6b4a9fb52b0a", + "content-hash": "25c2a1a4a2f2594adefe25ddb6a072fb", "packages": [ { "name": "aws/aws-crt-php", @@ -814,6 +814,67 @@ ], "time": "2025-03-06T22:45:56+00:00" }, + { + "name": "ezyang/htmlpurifier", + "version": "v4.19.0", + "source": { + "type": "git", + "url": "https://github.com/ezyang/htmlpurifier.git", + "reference": "b287d2a16aceffbf6e0295559b39662612b77fcf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ezyang/htmlpurifier/zipball/b287d2a16aceffbf6e0295559b39662612b77fcf", + "reference": "b287d2a16aceffbf6e0295559b39662612b77fcf", + "shasum": "" + }, + "require": { + "php": "~5.6.0 || ~7.0.0 || ~7.1.0 || ~7.2.0 || ~7.3.0 || ~7.4.0 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0" + }, + "require-dev": { + "cerdic/css-tidy": "^1.7 || ^2.0", + "simpletest/simpletest": "dev-master" + }, + "suggest": { + "cerdic/css-tidy": "If you want to use the filter 'Filter.ExtractStyleBlocks'.", + "ext-bcmath": "Used for unit conversion and imagecrash protection", + "ext-iconv": "Converts text to and from non-UTF-8 encodings", + "ext-tidy": "Used for pretty-printing HTML" + }, + "type": "library", + "autoload": { + "files": [ + "library/HTMLPurifier.composer.php" + ], + "psr-0": { + "HTMLPurifier": "library/" + }, + "exclude-from-classmap": [ + "/library/HTMLPurifier/Language/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-2.1-or-later" + ], + "authors": [ + { + "name": "Edward Z. Yang", + "email": "admin@htmlpurifier.org", + "homepage": "http://ezyang.com" + } + ], + "description": "Standards compliant HTML filter written in PHP", + "homepage": "http://htmlpurifier.org/", + "keywords": [ + "html" + ], + "support": { + "issues": "https://github.com/ezyang/htmlpurifier/issues", + "source": "https://github.com/ezyang/htmlpurifier/tree/v4.19.0" + }, + "time": "2025-10-17T16:34:55+00:00" + }, { "name": "firebase/php-jwt", "version": "v6.11.1", @@ -4947,6 +5008,72 @@ ], "time": "2025-01-13T13:04:43+00:00" }, + { + "name": "stevebauman/purify", + "version": "v6.3.1", + "source": { + "type": "git", + "url": "https://github.com/stevebauman/purify.git", + "reference": "3acb5e77904f420ce8aad8fa1c7f394e82daa500" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/stevebauman/purify/zipball/3acb5e77904f420ce8aad8fa1c7f394e82daa500", + "reference": "3acb5e77904f420ce8aad8fa1c7f394e82daa500", + "shasum": "" + }, + "require": { + "ezyang/htmlpurifier": "^4.17", + "illuminate/contracts": "^7.0|^8.0|^9.0|^10.0|^11.0|^12.0", + "illuminate/support": "^7.0|^8.0|^9.0|^10.0|^11.0|^12.0", + "php": ">=7.4" + }, + "require-dev": { + "orchestra/testbench": "^5.0|^6.0|^7.0|^8.0|^9.0|^10.0", + "phpunit/phpunit": "^8.0|^9.0|^10.0|^11.5.3" + }, + "type": "library", + "extra": { + "laravel": { + "aliases": { + "Purify": "Stevebauman\\Purify\\Facades\\Purify" + }, + "providers": [ + "Stevebauman\\Purify\\PurifyServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "Stevebauman\\Purify\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Steve Bauman", + "email": "steven_bauman@outlook.com" + } + ], + "description": "An HTML Purifier / Sanitizer for Laravel", + "keywords": [ + "Purifier", + "clean", + "cleaner", + "html", + "laravel", + "purification", + "purify" + ], + "support": { + "issues": "https://github.com/stevebauman/purify/issues", + "source": "https://github.com/stevebauman/purify/tree/v6.3.1" + }, + "time": "2025-05-21T16:53:09+00:00" + }, { "name": "symfony/clock", "version": "v8.0.0", diff --git a/resources/css/app.css b/resources/css/app.css index 46b9ca1..30cb7a1 100644 --- a/resources/css/app.css +++ b/resources/css/app.css @@ -59,6 +59,10 @@ @apply !mb-0 !leading-tight; } +[data-flux-description] a { + @apply text-accent underline hover:opacity-80; +} + input:focus[data-flux-control], textarea:focus[data-flux-control], select:focus[data-flux-control] { diff --git a/resources/views/livewire/plugins/recipe.blade.php b/resources/views/livewire/plugins/recipe.blade.php index 4be96cc..e8ab799 100644 --- a/resources/views/livewire/plugins/recipe.blade.php +++ b/resources/views/livewire/plugins/recipe.blade.php @@ -264,7 +264,7 @@ new class extends Component { $fieldKey = $field['keyname']; if (isset($this->configuration[$fieldKey])) { $value = $this->configuration[$fieldKey]; - + // For code fields, if the value is a JSON string and the original was an array, decode it if ($field['field_type'] === 'code' && is_string($value)) { $decoded = json_decode($value, true); @@ -274,7 +274,7 @@ new class extends Component { $value = $decoded; } } - + $configurationValues[$fieldKey] = $value; } } @@ -639,7 +639,14 @@ HTML; @php $fieldKey = $field['keyname'] ?? $field['key'] ?? $field['name']; $rawValue = $configuration[$fieldKey] ?? ($field['default'] ?? ''); - + # These are sanitized at PluginImportService when imported, safe to render HTML + $safeDescription = $field['description'] ?? ''; + $safeHelp = $field['help_text'] ?? ''; + + //Important: Sanitize with Purify to prevent XSS attacks + // $safeDescription = Stevebauman\Purify\Facades\Purify::clean($field['description'] ?? ''); + // $safeHelp = Stevebauman\Purify\Facades\Purify::clean($field['help_text'] ?? ''); + // For code fields, if the value is an array, JSON encode it if ($field['field_type'] === 'code' && is_array($rawValue)) { $currentValue = json_encode($rawValue, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); @@ -657,176 +664,211 @@ HTML; @endif @if($field['field_type'] === 'string' || $field['field_type'] === 'url') - + + {{ $field['name'] }} + {!! $safeDescription !!} + + {!! $safeHelp !!} + + @elseif($field['field_type'] === 'text') - + + {{ $field['name'] }} + {!! $safeDescription !!} + + {!! $safeHelp !!} + + @elseif($field['field_type'] === 'code') - + + {{ $field['name'] }} + {!! $safeDescription !!} + + {!! $safeHelp !!} + + @elseif($field['field_type'] === 'password') - + + {{ $field['name'] }} + {!! $safeDescription !!} + + {!! $safeHelp !!} + + @elseif($field['field_type'] === 'copyable') - + + {{ $field['name'] }} + {!! $safeDescription !!} + + {!! $safeHelp !!} + + @elseif($field['field_type'] === 'time_zone') - - - @foreach(timezone_identifiers_list() as $timezone) - - @endforeach - + + {{ $field['name'] }} + {!! $safeDescription !!} + + + @foreach(timezone_identifiers_list() as $timezone) + + @endforeach + + {!! $safeHelp !!} + + @elseif($field['field_type'] === 'number') - + + {{ $field['name'] }} + {!! $safeDescription !!} + + {!! $safeHelp !!} + + @elseif($field['field_type'] === 'boolean') - + + {{ $field['name'] }} + {!! $safeDescription !!} + + {!! $safeHelp !!} + + @elseif($field['field_type'] === 'date') - + + {{ $field['name'] }} + {!! $safeDescription !!} + + {!! $safeHelp !!} + + @elseif($field['field_type'] === 'time') - + + {{ $field['name'] }} + {!! $safeDescription !!} + + {!! $safeHelp !!} + + @elseif($field['field_type'] === 'select') @if(isset($field['multiple']) && $field['multiple'] === true) - - @if(isset($field['options']) && is_array($field['options'])) - @foreach($field['options'] as $option) - @if(is_array($option)) - @foreach($option as $label => $value) - - @endforeach - @else - @php - $key = mb_strtolower(str_replace(' ', '_', $option)); - @endphp - - @endif - @endforeach - @endif - + + {{ $field['name'] }} + {!! $safeDescription !!} + + @if(isset($field['options']) && is_array($field['options'])) + @foreach($field['options'] as $option) + @if(is_array($option)) + @foreach($option as $label => $value) + + @endforeach + @else + @php + $key = mb_strtolower(str_replace(' ', '_', $option)); + @endphp + + @endif + @endforeach + @endif + + {!! $safeHelp !!} + @else + + {{ $field['name'] }} + {!! $safeDescription !!} + + + @if(isset($field['options']) && is_array($field['options'])) + @foreach($field['options'] as $option) + @if(is_array($option)) + @foreach($option as $label => $value) + + @endforeach + @else + @php + $key = mb_strtolower(str_replace(' ', '_', $option)); + @endphp + + @endif + @endforeach + @endif + + {!! $safeHelp !!} + + @endif + + @elseif($field['field_type'] === 'xhrSelect') + + {{ $field['name'] }} + {!! $safeDescription !!} - @if(isset($field['options']) && is_array($field['options'])) - @foreach($field['options'] as $option) + @if(isset($xhrSelectOptions[$fieldKey]) && is_array($xhrSelectOptions[$fieldKey])) + @foreach($xhrSelectOptions[$fieldKey] as $option) @if(is_array($option)) - @foreach($option as $label => $value) - - @endforeach + @if(isset($option['id']) && isset($option['name'])) + {{-- xhrSelectSearch format: { 'id' => 'db-456', 'name' => 'Team Goals' } --}} + + @else + {{-- xhrSelect format: { 'Braves' => 123 } --}} + @foreach($option as $label => $value) + + @endforeach + @endif @else - @php - $key = mb_strtolower(str_replace(' ', '_', $option)); - @endphp - + @endif @endforeach @endif - @endif - @elseif($field['field_type'] === 'xhrSelect') - - - @if(isset($xhrSelectOptions[$fieldKey]) && is_array($xhrSelectOptions[$fieldKey])) - @foreach($xhrSelectOptions[$fieldKey] as $option) - @if(is_array($option)) - @if(isset($option['id']) && isset($option['name'])) - {{-- xhrSelectSearch format: { 'id' => 'db-456', 'name' => 'Team Goals' } --}} - - @else - {{-- xhrSelect format: { 'Braves' => 123 } --}} - @foreach($option as $label => $value) - - @endforeach - @endif - @else - - @endif - @endforeach - @endif - + {!! $safeHelp !!} + + @elseif($field['field_type'] === 'xhrSelectSearch')
{{ $field['name'] }} - {{ $field['description'] ?? '' }} + {!! $safeDescription !!} - {{ $field['help_text'] ?? '' }} + {!! $safeHelp !!} @if((isset($xhrSelectOptions[$fieldKey]) && is_array($xhrSelectOptions[$fieldKey]) && count($xhrSelectOptions[$fieldKey]) > 0) || !empty($currentValue)) @elseif($field['field_type'] === 'multi_string') - + + {{ $field['name'] }} + {!! $safeDescription !!} + + {!! $safeHelp !!} + @else Field type "{{ $field['field_type'] }}" not yet supported @endif diff --git a/tests/Unit/Models/PluginTest.php b/tests/Unit/Models/PluginTest.php index cf8ea97..49d3f2e 100644 --- a/tests/Unit/Models/PluginTest.php +++ b/tests/Unit/Models/PluginTest.php @@ -4,8 +4,12 @@ use App\Models\Plugin; use App\Models\User; use Carbon\Carbon; use Illuminate\Support\Facades\Http; +use Livewire\Volt\Volt; +use Illuminate\Foundation\Testing\RefreshDatabase; -uses(Illuminate\Foundation\Testing\RefreshDatabase::class); +use Tests\TestCase; + +uses(TestCase::class,RefreshDatabase::class); test('plugin has required attributes', function (): void { $plugin = Plugin::factory()->create([ @@ -679,3 +683,111 @@ test('plugin render includes utc_offset and time_zone_iana in trmnl.user context ->toContain('America/Chicago') ->and($rendered)->toMatch('/\|-?\d+/'); // Should contain a pipe followed by a number (offset in seconds) }); + + +/** + * Plugin security: XSS Payload Dataset + * [Input, Expected to See, Dangerous parts that must be Missing] + */ +dataset('xss_vectors', [ + 'standard_script' => [ + 'Safe ', + 'Safe', + ['', + 'Unclosed tag', + ['', - 'Safe', - ['', - 'Unclosed tag', - ['', 'Safe ', '', 'Safe ', '', 'Safe ', ' + + + + + + +
+
+ + + +
+ + + \ No newline at end of file diff --git a/public/mirror/manifest.json b/public/mirror/manifest.json new file mode 100644 index 0000000..4d44e44 --- /dev/null +++ b/public/mirror/manifest.json @@ -0,0 +1,7 @@ +{ + "name": "TRMNL BYOS Laravel Mirror", + "short_name": "TRMNL BYOS", + "display": "standalone", + "background_color": "#ffffff", + "theme_color": "#ffffff" +} From ddce3947c61a703c77a695f98835662778a8932a Mon Sep 17 00:00:00 2001 From: Gabriele Lauricella Date: Thu, 8 Jan 2026 19:04:21 +0100 Subject: [PATCH 029/104] feat: enhanced web mirror trmnl client --- public/mirror/index.html | 116 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 114 insertions(+), 2 deletions(-) diff --git a/public/mirror/index.html b/public/mirror/index.html index 2c5fcf6..64746fe 100644 --- a/public/mirror/index.html +++ b/public/mirror/index.html @@ -18,6 +18,7 @@ - + +
diff --git a/resources/views/recipes/weather.blade.php b/resources/views/recipes/weather.blade.php index 0d8045f..be49361 100644 --- a/resources/views/recipes/weather.blade.php +++ b/resources/views/recipes/weather.blade.php @@ -5,7 +5,7 @@
+ src="{{ config('services.trmnl.base_url') }}/images/plugins/weather/wi-thermometer.svg">
@@ -21,7 +21,7 @@
- {{-- --}} + {{-- --}}
- {{-- --}} + {{-- --}}
{{Arr::get($data, 'properties.timeseries.0.data.instant.details.relative_humidity', 'N/A')}}% @@ -44,7 +44,7 @@
- {{-- --}} + {{-- --}}
@else + href="{{ config('services.trmnl.base_url') }}/css/{{ config('trmnl-blade.framework_version', '1.2.0') }}/plugins.css"> @endif @if (config('trmnl-blade.framework_js_url')) @else - + @endif {{ $title ?? config('app.name') }} diff --git a/tests/Feature/Console/FirmwareCheckCommandTest.php b/tests/Feature/Console/FirmwareCheckCommandTest.php index 459a035..289ff2f 100644 --- a/tests/Feature/Console/FirmwareCheckCommandTest.php +++ b/tests/Feature/Console/FirmwareCheckCommandTest.php @@ -17,8 +17,10 @@ test('firmware check command has correct signature', function (): void { test('firmware check command runs without errors', function (): void { // Mock the firmware API response + $baseUrl = config('services.trmnl.base_url'); + Http::fake([ - 'https://usetrmnl.com/api/firmware/latest' => Http::response([ + $baseUrl.'/api/firmware/latest' => Http::response([ 'version' => '1.0.0', 'url' => 'https://example.com/firmware.bin', ], 200), @@ -33,8 +35,10 @@ test('firmware check command runs without errors', function (): void { test('firmware check command runs with download flag', function (): void { // Mock the firmware API response + $baseUrl = config('services.trmnl.base_url'); + Http::fake([ - 'https://usetrmnl.com/api/firmware/latest' => Http::response([ + $baseUrl.'/api/firmware/latest' => Http::response([ 'version' => '1.0.0', 'url' => 'https://example.com/firmware.bin', ], 200), @@ -57,8 +61,10 @@ test('firmware check command runs with download flag', function (): void { test('firmware check command can run successfully', function (): void { // Mock the firmware API response + $baseUrl = config('services.trmnl.base_url'); + Http::fake([ - 'https://usetrmnl.com/api/firmware/latest' => Http::response([ + $baseUrl.'/api/firmware/latest' => Http::response([ 'version' => '1.0.0', 'url' => 'https://example.com/firmware.bin', ], 200), diff --git a/tests/Feature/Jobs/FetchDeviceModelsJobTest.php b/tests/Feature/Jobs/FetchDeviceModelsJobTest.php index f0be135..f74a6fd 100644 --- a/tests/Feature/Jobs/FetchDeviceModelsJobTest.php +++ b/tests/Feature/Jobs/FetchDeviceModelsJobTest.php @@ -29,7 +29,7 @@ test('fetch device models job can be dispatched', function (): void { test('fetch device models job handles successful api response', function (): void { Http::fake([ 'usetrmnl.com/api/palettes' => Http::response(['data' => []], 200), - 'usetrmnl.com/api/models' => Http::response([ + config('services.trmnl.base_url').'/api/models' => Http::response([ 'data' => [ [ 'name' => 'test-model', @@ -82,7 +82,7 @@ test('fetch device models job handles successful api response', function (): voi test('fetch device models job handles multiple device models', function (): void { Http::fake([ 'usetrmnl.com/api/palettes' => Http::response(['data' => []], 200), - 'usetrmnl.com/api/models' => Http::response([ + config('services.trmnl.base_url').'/api/models' => Http::response([ 'data' => [ [ 'name' => 'model-1', @@ -136,7 +136,7 @@ test('fetch device models job handles multiple device models', function (): void test('fetch device models job handles empty data array', function (): void { Http::fake([ 'usetrmnl.com/api/palettes' => Http::response(['data' => []], 200), - 'usetrmnl.com/api/models' => Http::response([ + config('services.trmnl.base_url').'/api/models' => Http::response([ 'data' => [], ], 200), ]); @@ -158,7 +158,7 @@ test('fetch device models job handles empty data array', function (): void { test('fetch device models job handles missing data field', function (): void { Http::fake([ 'usetrmnl.com/api/palettes' => Http::response(['data' => []], 200), - 'usetrmnl.com/api/models' => Http::response([ + config('services.trmnl.base_url').'/api/models' => Http::response([ 'message' => 'No data available', ], 200), ]); @@ -180,7 +180,7 @@ test('fetch device models job handles missing data field', function (): void { test('fetch device models job handles non-array data', function (): void { Http::fake([ 'usetrmnl.com/api/palettes' => Http::response(['data' => []], 200), - 'usetrmnl.com/api/models' => Http::response([ + config('services.trmnl.base_url').'/api/models' => Http::response([ 'data' => 'invalid-data', ], 200), ]); @@ -202,7 +202,7 @@ test('fetch device models job handles non-array data', function (): void { test('fetch device models job handles api failure', function (): void { Http::fake([ 'usetrmnl.com/api/palettes' => Http::response(['data' => []], 200), - 'usetrmnl.com/api/models' => Http::response([ + config('services.trmnl.base_url').'/api/models' => Http::response([ 'error' => 'Internal Server Error', ], 500), ]); @@ -227,7 +227,7 @@ test('fetch device models job handles api failure', function (): void { test('fetch device models job handles network exception', function (): void { Http::fake([ 'usetrmnl.com/api/palettes' => Http::response(['data' => []], 200), - 'usetrmnl.com/api/models' => function (): void { + config('services.trmnl.base_url').'/api/models' => function (): void { throw new Exception('Network connection failed'); }, ]); @@ -249,7 +249,7 @@ test('fetch device models job handles network exception', function (): void { test('fetch device models job handles device model with missing name', function (): void { Http::fake([ 'usetrmnl.com/api/palettes' => Http::response(['data' => []], 200), - 'usetrmnl.com/api/models' => Http::response([ + config('services.trmnl.base_url').'/api/models' => Http::response([ 'data' => [ [ 'label' => 'Model without name', @@ -280,7 +280,7 @@ test('fetch device models job handles device model with missing name', function test('fetch device models job handles device model with partial data', function (): void { Http::fake([ 'usetrmnl.com/api/palettes' => Http::response(['data' => []], 200), - 'usetrmnl.com/api/models' => Http::response([ + config('services.trmnl.base_url').'/api/models' => Http::response([ 'data' => [ [ 'name' => 'minimal-model', @@ -329,7 +329,7 @@ test('fetch device models job updates existing device model', function (): void Http::fake([ 'usetrmnl.com/api/palettes' => Http::response(['data' => []], 200), - 'usetrmnl.com/api/models' => Http::response([ + config('services.trmnl.base_url').'/api/models' => Http::response([ 'data' => [ [ 'name' => 'existing-model', @@ -372,7 +372,7 @@ test('fetch device models job updates existing device model', function (): void test('fetch device models job handles processing exception for individual model', function (): void { Http::fake([ 'usetrmnl.com/api/palettes' => Http::response(['data' => []], 200), - 'usetrmnl.com/api/models' => Http::response([ + config('services.trmnl.base_url').'/api/models' => Http::response([ 'data' => [ [ 'name' => 'valid-model', diff --git a/tests/Feature/Jobs/FirmwarePollJobTest.php b/tests/Feature/Jobs/FirmwarePollJobTest.php index 74c3cf7..4861d4d 100644 --- a/tests/Feature/Jobs/FirmwarePollJobTest.php +++ b/tests/Feature/Jobs/FirmwarePollJobTest.php @@ -10,8 +10,10 @@ beforeEach(function (): void { }); test('it creates new firmware record when polling', function (): void { + $baseUrl = config('services.trmnl.base_url'); + Http::fake([ - 'https://usetrmnl.com/api/firmware/latest' => Http::response([ + $baseUrl.'/api/firmware/latest' => Http::response([ 'version' => '1.0.0', 'url' => 'https://example.com/firmware.bin', ], 200), @@ -32,8 +34,10 @@ test('it updates existing firmware record when polling', function (): void { 'latest' => true, ]); + $baseUrl = config('services.trmnl.base_url'); + Http::fake([ - 'https://usetrmnl.com/api/firmware/latest' => Http::response([ + $baseUrl.'/api/firmware/latest' => Http::response([ 'version' => '1.0.0', 'url' => 'https://new-url.com/firmware.bin', ], 200), @@ -52,8 +56,10 @@ test('it marks previous firmware as not latest when new version is found', funct 'latest' => true, ]); + $baseUrl = config('services.trmnl.base_url'); + Http::fake([ - 'https://usetrmnl.com/api/firmware/latest' => Http::response([ + $baseUrl.'/api/firmware/latest' => Http::response([ 'version' => '1.1.0', 'url' => 'https://example.com/firmware.bin', ], 200), @@ -66,8 +72,10 @@ test('it marks previous firmware as not latest when new version is found', funct }); test('it handles connection exception gracefully', function (): void { + $baseUrl = config('services.trmnl.base_url'); + Http::fake([ - 'https://usetrmnl.com/api/firmware/latest' => function (): void { + $baseUrl.'/api/firmware/latest' => function (): void { throw new ConnectionException('Connection failed'); }, ]); @@ -79,8 +87,10 @@ test('it handles connection exception gracefully', function (): void { }); test('it handles invalid response gracefully', function (): void { + $baseUrl = config('services.trmnl.base_url'); + Http::fake([ - 'https://usetrmnl.com/api/firmware/latest' => Http::response(null, 200), + $baseUrl.'/api/firmware/latest' => Http::response(null, 200), ]); (new FirmwarePollJob)->handle(); @@ -90,8 +100,10 @@ test('it handles invalid response gracefully', function (): void { }); test('it handles missing version in response gracefully', function (): void { + $baseUrl = config('services.trmnl.base_url'); + Http::fake([ - 'https://usetrmnl.com/api/firmware/latest' => Http::response([ + $baseUrl.'/api/firmware/latest' => Http::response([ 'url' => 'https://example.com/firmware.bin', ], 200), ]); @@ -103,8 +115,10 @@ test('it handles missing version in response gracefully', function (): void { }); test('it handles missing url in response gracefully', function (): void { + $baseUrl = config('services.trmnl.base_url'); + Http::fake([ - 'https://usetrmnl.com/api/firmware/latest' => Http::response([ + $baseUrl.'/api/firmware/latest' => Http::response([ 'version' => '1.0.0', ], 200), ]); diff --git a/tests/Feature/Livewire/Catalog/CatalogTrmnlTest.php b/tests/Feature/Livewire/Catalog/CatalogTrmnlTest.php index 536f6ad..32fcd94 100644 --- a/tests/Feature/Livewire/Catalog/CatalogTrmnlTest.php +++ b/tests/Feature/Livewire/Catalog/CatalogTrmnlTest.php @@ -8,7 +8,7 @@ use Livewire\Livewire; it('loads newest TRMNL recipes on mount', function (): void { Http::fake([ - 'usetrmnl.com/recipes.json*' => Http::response([ + config('services.trmnl.base_url').'/recipes.json*' => Http::response([ 'data' => [ [ 'id' => 123, @@ -33,7 +33,7 @@ it('loads newest TRMNL recipes on mount', function (): void { it('shows preview button when screenshot_url is provided', function (): void { Http::fake([ - 'usetrmnl.com/recipes.json*' => Http::response([ + config('services.trmnl.base_url').'/recipes.json*' => Http::response([ 'data' => [ [ 'id' => 123, @@ -57,7 +57,7 @@ it('shows preview button when screenshot_url is provided', function (): void { it('searches TRMNL recipes when search term is provided', function (): void { Http::fake([ // First call (mount -> newest) - 'usetrmnl.com/recipes.json?*' => Http::sequence() + config('services.trmnl.base_url').'/recipes.json?*' => Http::sequence() ->push([ 'data' => [ [ @@ -98,7 +98,7 @@ it('installs plugin successfully when user is authenticated', function (): void $user = User::factory()->create(); Http::fake([ - 'usetrmnl.com/recipes.json*' => Http::response([ + config('services.trmnl.base_url').'/recipes.json*' => Http::response([ 'data' => [ [ 'id' => 123, @@ -110,7 +110,7 @@ it('installs plugin successfully when user is authenticated', function (): void ], ], ], 200), - 'usetrmnl.com/api/plugin_settings/123/archive*' => Http::response('fake zip content', 200), + config('services.trmnl.base_url').'/api/plugin_settings/123/archive*' => Http::response('fake zip content', 200), ]); $this->actingAs($user); @@ -125,7 +125,7 @@ it('installs plugin successfully when user is authenticated', function (): void it('shows error when user is not authenticated', function (): void { Http::fake([ - 'usetrmnl.com/recipes.json*' => Http::response([ + config('services.trmnl.base_url').'/recipes.json*' => Http::response([ 'data' => [ [ 'id' => 123, @@ -151,7 +151,7 @@ it('shows error when plugin installation fails', function (): void { $user = User::factory()->create(); Http::fake([ - 'usetrmnl.com/recipes.json*' => Http::response([ + config('services.trmnl.base_url').'/recipes.json*' => Http::response([ 'data' => [ [ 'id' => 123, @@ -163,7 +163,7 @@ it('shows error when plugin installation fails', function (): void { ], ], ], 200), - 'usetrmnl.com/api/plugin_settings/123/archive*' => Http::response('invalid zip content', 200), + config('services.trmnl.base_url').'/api/plugin_settings/123/archive*' => Http::response('invalid zip content', 200), ]); $this->actingAs($user); @@ -178,7 +178,7 @@ it('shows error when plugin installation fails', function (): void { it('previews a recipe with async fetch', function (): void { Http::fake([ - 'usetrmnl.com/recipes.json*' => Http::response([ + config('services.trmnl.base_url').'/recipes.json*' => Http::response([ 'data' => [ [ 'id' => 123, @@ -190,7 +190,7 @@ it('previews a recipe with async fetch', function (): void { ], ], ], 200), - 'usetrmnl.com/recipes/123.json' => Http::response([ + config('services.trmnl.base_url').'/recipes/123.json' => Http::response([ 'data' => [ 'id' => 123, 'name' => 'Weather Chum Updated', @@ -216,7 +216,7 @@ it('previews a recipe with async fetch', function (): void { it('supports pagination and loading more recipes', function (): void { Http::fake([ - 'usetrmnl.com/recipes.json?sort-by=newest&page=1' => Http::response([ + config('services.trmnl.base_url').'/recipes.json?sort-by=newest&page=1' => Http::response([ 'data' => [ [ 'id' => 1, @@ -229,7 +229,7 @@ it('supports pagination and loading more recipes', function (): void { ], 'next_page_url' => '/recipes.json?page=2', ], 200), - 'usetrmnl.com/recipes.json?sort-by=newest&page=2' => Http::response([ + config('services.trmnl.base_url').'/recipes.json?sort-by=newest&page=2' => Http::response([ 'data' => [ [ 'id' => 2, @@ -258,7 +258,7 @@ it('supports pagination and loading more recipes', function (): void { it('resets pagination when search term changes', function (): void { Http::fake([ - 'usetrmnl.com/recipes.json?sort-by=newest&page=1' => Http::sequence() + config('services.trmnl.base_url').'/recipes.json?sort-by=newest&page=1' => Http::sequence() ->push([ 'data' => [['id' => 1, 'name' => 'Initial 1']], 'next_page_url' => '/recipes.json?page=2', @@ -267,7 +267,7 @@ it('resets pagination when search term changes', function (): void { 'data' => [['id' => 3, 'name' => 'Initial 1 Again']], 'next_page_url' => null, ]), - 'usetrmnl.com/recipes.json?search=weather&sort-by=newest&page=1' => Http::response([ + config('services.trmnl.base_url').'/recipes.json?search=weather&sort-by=newest&page=1' => Http::response([ 'data' => [['id' => 2, 'name' => 'Weather Result']], 'next_page_url' => null, ]), diff --git a/tests/Feature/PluginRequiredConfigurationTest.php b/tests/Feature/PluginRequiredConfigurationTest.php index 51e1b76..5f46321 100644 --- a/tests/Feature/PluginRequiredConfigurationTest.php +++ b/tests/Feature/PluginRequiredConfigurationTest.php @@ -227,7 +227,7 @@ test('hasMissingRequiredConfigurationFields returns true when required xhrSelect 'field_type' => 'xhrSelect', 'name' => 'Baseball Team', 'description' => 'Select your team', - 'endpoint' => 'https://usetrmnl.com/custom_plugin_example_xhr_select.json', + 'endpoint' => config('services.trmnl.base_url').'/custom_plugin_example_xhr_select.json', // Not marked as optional, so it's required ], ], @@ -252,7 +252,7 @@ test('hasMissingRequiredConfigurationFields returns false when required xhrSelec 'field_type' => 'xhrSelect', 'name' => 'Baseball Team', 'description' => 'Select your team', - 'endpoint' => 'https://usetrmnl.com/custom_plugin_example_xhr_select.json', + 'endpoint' => config('services.trmnl.base_url').'/custom_plugin_example_xhr_select.json', // Not marked as optional, so it's required ], ], From c59f3deed9d144ec9b227e24b52615d300db02b0 Mon Sep 17 00:00:00 2001 From: Benjamin Nussbaum Date: Wed, 28 Jan 2026 12:23:19 +0100 Subject: [PATCH 070/104] chore: update dependencies --- .gitignore | 2 + composer.json | 2 +- composer.lock | 360 +++++++++++----------- package-lock.json | 764 +++++++++++++++++++++++++--------------------- 4 files changed, 591 insertions(+), 537 deletions(-) diff --git a/.gitignore b/.gitignore index 0eb46d3..dd96cc6 100644 --- a/.gitignore +++ b/.gitignore @@ -37,3 +37,5 @@ yarn-error.log /.claude /AGENTS.md /opencode.json +/.cursor +/.opencode diff --git a/composer.json b/composer.json index 4438dbf..555ad57 100644 --- a/composer.json +++ b/composer.json @@ -35,7 +35,7 @@ "require-dev": { "fakerphp/faker": "^1.23", "larastan/larastan": "^3.0", - "laravel/boost": "^1.0", + "laravel/boost": "^2.0", "laravel/pail": "^1.2.2", "laravel/pint": "^1.18", "laravel/sail": "^1.41", diff --git a/composer.lock b/composer.lock index 6617cc0..df71e7b 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": "23163ec9d3efca34357cab8ee5219529", + "content-hash": "324bc90c0d60675c736e4001ba845a5a", "packages": [ { "name": "aws/aws-crt-php", @@ -62,16 +62,16 @@ }, { "name": "aws/aws-sdk-php", - "version": "3.369.18", + "version": "3.369.21", "source": { "type": "git", "url": "https://github.com/aws/aws-sdk-php.git", - "reference": "48d809eda94dd528ef539cff827e7542ac01cce6" + "reference": "7076af00534135cbbf6cc19eb2521124a3549f0d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/48d809eda94dd528ef539cff827e7542ac01cce6", - "reference": "48d809eda94dd528ef539cff827e7542ac01cce6", + "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/7076af00534135cbbf6cc19eb2521124a3549f0d", + "reference": "7076af00534135cbbf6cc19eb2521124a3549f0d", "shasum": "" }, "require": { @@ -153,9 +153,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.369.18" + "source": "https://github.com/aws/aws-sdk-php/tree/3.369.21" }, - "time": "2026-01-22T19:05:57+00:00" + "time": "2026-01-27T19:14:48+00:00" }, { "name": "bacon/bacon-qr-code", @@ -1831,16 +1831,16 @@ }, { "name": "laravel/fortify", - "version": "v1.33.0", + "version": "v1.34.0", "source": { "type": "git", "url": "https://github.com/laravel/fortify.git", - "reference": "e0666dabeec0b6428678af1d51f436dcfb24e3a9" + "reference": "c322715f2786210a722ed56966f7c9877b653b25" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/fortify/zipball/e0666dabeec0b6428678af1d51f436dcfb24e3a9", - "reference": "e0666dabeec0b6428678af1d51f436dcfb24e3a9", + "url": "https://api.github.com/repos/laravel/fortify/zipball/c322715f2786210a722ed56966f7c9877b653b25", + "reference": "c322715f2786210a722ed56966f7c9877b653b25", "shasum": "" }, "require": { @@ -1890,20 +1890,20 @@ "issues": "https://github.com/laravel/fortify/issues", "source": "https://github.com/laravel/fortify" }, - "time": "2025-12-15T14:48:33+00:00" + "time": "2026-01-26T10:23:19+00:00" }, { "name": "laravel/framework", - "version": "v12.48.1", + "version": "v12.49.0", "source": { "type": "git", "url": "https://github.com/laravel/framework.git", - "reference": "0f0974a9769378ccd9c9935c09b9927f3a606830" + "reference": "4bde4530545111d8bdd1de6f545fa8824039fcb5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/framework/zipball/0f0974a9769378ccd9c9935c09b9927f3a606830", - "reference": "0f0974a9769378ccd9c9935c09b9927f3a606830", + "url": "https://api.github.com/repos/laravel/framework/zipball/4bde4530545111d8bdd1de6f545fa8824039fcb5", + "reference": "4bde4530545111d8bdd1de6f545fa8824039fcb5", "shasum": "" }, "require": { @@ -2112,20 +2112,20 @@ "issues": "https://github.com/laravel/framework/issues", "source": "https://github.com/laravel/framework" }, - "time": "2026-01-20T16:12:36+00:00" + "time": "2026-01-28T03:40:49+00:00" }, { "name": "laravel/prompts", - "version": "v0.3.10", + "version": "v0.3.11", "source": { "type": "git", "url": "https://github.com/laravel/prompts.git", - "reference": "360ba095ef9f51017473505191fbd4ab73e1cab3" + "reference": "dd2a2ed95acacbcccd32fd98dee4c946ae7a7217" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/prompts/zipball/360ba095ef9f51017473505191fbd4ab73e1cab3", - "reference": "360ba095ef9f51017473505191fbd4ab73e1cab3", + "url": "https://api.github.com/repos/laravel/prompts/zipball/dd2a2ed95acacbcccd32fd98dee4c946ae7a7217", + "reference": "dd2a2ed95acacbcccd32fd98dee4c946ae7a7217", "shasum": "" }, "require": { @@ -2169,22 +2169,22 @@ "description": "Add beautiful and user-friendly forms to your command-line applications.", "support": { "issues": "https://github.com/laravel/prompts/issues", - "source": "https://github.com/laravel/prompts/tree/v0.3.10" + "source": "https://github.com/laravel/prompts/tree/v0.3.11" }, - "time": "2026-01-13T20:29:29+00:00" + "time": "2026-01-27T02:55:06+00:00" }, { "name": "laravel/sanctum", - "version": "v4.2.4", + "version": "v4.3.0", "source": { "type": "git", "url": "https://github.com/laravel/sanctum.git", - "reference": "dadd2277ff0f05cdb435c8b6a0bcedcf3b5519a9" + "reference": "c978c82b2b8ab685468a7ca35224497d541b775a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/sanctum/zipball/dadd2277ff0f05cdb435c8b6a0bcedcf3b5519a9", - "reference": "dadd2277ff0f05cdb435c8b6a0bcedcf3b5519a9", + "url": "https://api.github.com/repos/laravel/sanctum/zipball/c978c82b2b8ab685468a7ca35224497d541b775a", + "reference": "c978c82b2b8ab685468a7ca35224497d541b775a", "shasum": "" }, "require": { @@ -2234,7 +2234,7 @@ "issues": "https://github.com/laravel/sanctum/issues", "source": "https://github.com/laravel/sanctum" }, - "time": "2026-01-15T14:37:16+00:00" + "time": "2026-01-22T22:27:01+00:00" }, { "name": "laravel/serializable-closure", @@ -3194,16 +3194,16 @@ }, { "name": "livewire/livewire", - "version": "v4.0.3", + "version": "v4.1.0", "source": { "type": "git", "url": "https://github.com/livewire/livewire.git", - "reference": "11afe7b36dbb1d98e073551c217f87b2b6911f81" + "reference": "4ae4ee18448f8e9d97b68c8c091b2b597f852a6f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/livewire/livewire/zipball/11afe7b36dbb1d98e073551c217f87b2b6911f81", - "reference": "11afe7b36dbb1d98e073551c217f87b2b6911f81", + "url": "https://api.github.com/repos/livewire/livewire/zipball/4ae4ee18448f8e9d97b68c8c091b2b597f852a6f", + "reference": "4ae4ee18448f8e9d97b68c8c091b2b597f852a6f", "shasum": "" }, "require": { @@ -3258,7 +3258,7 @@ "description": "A front-end framework for Laravel.", "support": { "issues": "https://github.com/livewire/livewire/issues", - "source": "https://github.com/livewire/livewire/tree/v4.0.3" + "source": "https://github.com/livewire/livewire/tree/v4.1.0" }, "funding": [ { @@ -3266,7 +3266,7 @@ "type": "github" } ], - "time": "2026-01-23T06:13:46+00:00" + "time": "2026-01-27T02:21:37+00:00" }, { "name": "maennchen/zipstream-php", @@ -4277,16 +4277,16 @@ }, { "name": "phpseclib/phpseclib", - "version": "3.0.48", + "version": "3.0.49", "source": { "type": "git", "url": "https://github.com/phpseclib/phpseclib.git", - "reference": "64065a5679c50acb886e82c07aa139b0f757bb89" + "reference": "6233a1e12584754e6b5daa69fe1289b47775c1b9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/64065a5679c50acb886e82c07aa139b0f757bb89", - "reference": "64065a5679c50acb886e82c07aa139b0f757bb89", + "url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/6233a1e12584754e6b5daa69fe1289b47775c1b9", + "reference": "6233a1e12584754e6b5daa69fe1289b47775c1b9", "shasum": "" }, "require": { @@ -4367,7 +4367,7 @@ ], "support": { "issues": "https://github.com/phpseclib/phpseclib/issues", - "source": "https://github.com/phpseclib/phpseclib/tree/3.0.48" + "source": "https://github.com/phpseclib/phpseclib/tree/3.0.49" }, "funding": [ { @@ -4383,20 +4383,20 @@ "type": "tidelift" } ], - "time": "2025-12-15T11:51:42+00:00" + "time": "2026-01-27T09:17:28+00:00" }, { "name": "phpstan/phpdoc-parser", - "version": "2.3.1", + "version": "2.3.2", "source": { "type": "git", "url": "https://github.com/phpstan/phpdoc-parser.git", - "reference": "16dbf9937da8d4528ceb2145c9c7c0bd29e26374" + "reference": "a004701b11273a26cd7955a61d67a7f1e525a45a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/16dbf9937da8d4528ceb2145c9c7c0bd29e26374", - "reference": "16dbf9937da8d4528ceb2145c9c7c0bd29e26374", + "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/a004701b11273a26cd7955a61d67a7f1e525a45a", + "reference": "a004701b11273a26cd7955a61d67a7f1e525a45a", "shasum": "" }, "require": { @@ -4428,9 +4428,9 @@ "description": "PHPDoc parser with support for nullable, intersection and generic types", "support": { "issues": "https://github.com/phpstan/phpdoc-parser/issues", - "source": "https://github.com/phpstan/phpdoc-parser/tree/2.3.1" + "source": "https://github.com/phpstan/phpdoc-parser/tree/2.3.2" }, - "time": "2026-01-12T11:33:04+00:00" + "time": "2026-01-25T14:56:51+00:00" }, { "name": "pragmarx/google2fa", @@ -5593,16 +5593,16 @@ }, { "name": "symfony/console", - "version": "v7.4.3", + "version": "v7.4.4", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "732a9ca6cd9dfd940c639062d5edbde2f6727fb6" + "reference": "41e38717ac1dd7a46b6bda7d6a82af2d98a78894" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/732a9ca6cd9dfd940c639062d5edbde2f6727fb6", - "reference": "732a9ca6cd9dfd940c639062d5edbde2f6727fb6", + "url": "https://api.github.com/repos/symfony/console/zipball/41e38717ac1dd7a46b6bda7d6a82af2d98a78894", + "reference": "41e38717ac1dd7a46b6bda7d6a82af2d98a78894", "shasum": "" }, "require": { @@ -5667,7 +5667,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v7.4.3" + "source": "https://github.com/symfony/console/tree/v7.4.4" }, "funding": [ { @@ -5687,7 +5687,7 @@ "type": "tidelift" } ], - "time": "2025-12-23T14:50:43+00:00" + "time": "2026-01-13T11:36:38+00:00" }, { "name": "symfony/css-selector", @@ -5827,16 +5827,16 @@ }, { "name": "symfony/error-handler", - "version": "v7.4.0", + "version": "v7.4.4", "source": { "type": "git", "url": "https://github.com/symfony/error-handler.git", - "reference": "48be2b0653594eea32dcef130cca1c811dcf25c2" + "reference": "8da531f364ddfee53e36092a7eebbbd0b775f6b8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/error-handler/zipball/48be2b0653594eea32dcef130cca1c811dcf25c2", - "reference": "48be2b0653594eea32dcef130cca1c811dcf25c2", + "url": "https://api.github.com/repos/symfony/error-handler/zipball/8da531f364ddfee53e36092a7eebbbd0b775f6b8", + "reference": "8da531f364ddfee53e36092a7eebbbd0b775f6b8", "shasum": "" }, "require": { @@ -5885,7 +5885,7 @@ "description": "Provides tools to manage errors and ease debugging PHP code", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/error-handler/tree/v7.4.0" + "source": "https://github.com/symfony/error-handler/tree/v7.4.4" }, "funding": [ { @@ -5905,20 +5905,20 @@ "type": "tidelift" } ], - "time": "2025-11-05T14:29:59+00:00" + "time": "2026-01-20T16:42:42+00:00" }, { "name": "symfony/event-dispatcher", - "version": "v8.0.0", + "version": "v8.0.4", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher.git", - "reference": "573f95783a2ec6e38752979db139f09fec033f03" + "reference": "99301401da182b6cfaa4700dbe9987bb75474b47" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/573f95783a2ec6e38752979db139f09fec033f03", - "reference": "573f95783a2ec6e38752979db139f09fec033f03", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/99301401da182b6cfaa4700dbe9987bb75474b47", + "reference": "99301401da182b6cfaa4700dbe9987bb75474b47", "shasum": "" }, "require": { @@ -5970,7 +5970,7 @@ "description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/event-dispatcher/tree/v8.0.0" + "source": "https://github.com/symfony/event-dispatcher/tree/v8.0.4" }, "funding": [ { @@ -5990,7 +5990,7 @@ "type": "tidelift" } ], - "time": "2025-10-30T14:17:19+00:00" + "time": "2026-01-05T11:45:55+00:00" }, { "name": "symfony/event-dispatcher-contracts", @@ -6140,16 +6140,16 @@ }, { "name": "symfony/finder", - "version": "v7.4.3", + "version": "v7.4.5", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "fffe05569336549b20a1be64250b40516d6e8d06" + "reference": "ad4daa7c38668dcb031e63bc99ea9bd42196a2cb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/fffe05569336549b20a1be64250b40516d6e8d06", - "reference": "fffe05569336549b20a1be64250b40516d6e8d06", + "url": "https://api.github.com/repos/symfony/finder/zipball/ad4daa7c38668dcb031e63bc99ea9bd42196a2cb", + "reference": "ad4daa7c38668dcb031e63bc99ea9bd42196a2cb", "shasum": "" }, "require": { @@ -6184,7 +6184,7 @@ "description": "Finds files and directories via an intuitive fluent interface", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/finder/tree/v7.4.3" + "source": "https://github.com/symfony/finder/tree/v7.4.5" }, "funding": [ { @@ -6204,20 +6204,20 @@ "type": "tidelift" } ], - "time": "2025-12-23T14:50:43+00:00" + "time": "2026-01-26T15:07:59+00:00" }, { "name": "symfony/http-foundation", - "version": "v7.4.3", + "version": "v7.4.5", "source": { "type": "git", "url": "https://github.com/symfony/http-foundation.git", - "reference": "a70c745d4cea48dbd609f4075e5f5cbce453bd52" + "reference": "446d0db2b1f21575f1284b74533e425096abdfb6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-foundation/zipball/a70c745d4cea48dbd609f4075e5f5cbce453bd52", - "reference": "a70c745d4cea48dbd609f4075e5f5cbce453bd52", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/446d0db2b1f21575f1284b74533e425096abdfb6", + "reference": "446d0db2b1f21575f1284b74533e425096abdfb6", "shasum": "" }, "require": { @@ -6266,7 +6266,7 @@ "description": "Defines an object-oriented layer for the HTTP specification", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/http-foundation/tree/v7.4.3" + "source": "https://github.com/symfony/http-foundation/tree/v7.4.5" }, "funding": [ { @@ -6286,20 +6286,20 @@ "type": "tidelift" } ], - "time": "2025-12-23T14:23:49+00:00" + "time": "2026-01-27T16:16:02+00:00" }, { "name": "symfony/http-kernel", - "version": "v7.4.3", + "version": "v7.4.5", "source": { "type": "git", "url": "https://github.com/symfony/http-kernel.git", - "reference": "885211d4bed3f857b8c964011923528a55702aa5" + "reference": "229eda477017f92bd2ce7615d06222ec0c19e82a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-kernel/zipball/885211d4bed3f857b8c964011923528a55702aa5", - "reference": "885211d4bed3f857b8c964011923528a55702aa5", + "url": "https://api.github.com/repos/symfony/http-kernel/zipball/229eda477017f92bd2ce7615d06222ec0c19e82a", + "reference": "229eda477017f92bd2ce7615d06222ec0c19e82a", "shasum": "" }, "require": { @@ -6385,7 +6385,7 @@ "description": "Provides a structured process for converting a Request into a Response", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/http-kernel/tree/v7.4.3" + "source": "https://github.com/symfony/http-kernel/tree/v7.4.5" }, "funding": [ { @@ -6405,20 +6405,20 @@ "type": "tidelift" } ], - "time": "2025-12-31T08:43:57+00:00" + "time": "2026-01-28T10:33:42+00:00" }, { "name": "symfony/mailer", - "version": "v7.4.3", + "version": "v7.4.4", "source": { "type": "git", "url": "https://github.com/symfony/mailer.git", - "reference": "e472d35e230108231ccb7f51eb6b2100cac02ee4" + "reference": "7b750074c40c694ceb34cb926d6dffee231c5cd6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/mailer/zipball/e472d35e230108231ccb7f51eb6b2100cac02ee4", - "reference": "e472d35e230108231ccb7f51eb6b2100cac02ee4", + "url": "https://api.github.com/repos/symfony/mailer/zipball/7b750074c40c694ceb34cb926d6dffee231c5cd6", + "reference": "7b750074c40c694ceb34cb926d6dffee231c5cd6", "shasum": "" }, "require": { @@ -6469,7 +6469,7 @@ "description": "Helps sending emails", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/mailer/tree/v7.4.3" + "source": "https://github.com/symfony/mailer/tree/v7.4.4" }, "funding": [ { @@ -6489,20 +6489,20 @@ "type": "tidelift" } ], - "time": "2025-12-16T08:02:06+00:00" + "time": "2026-01-08T08:25:11+00:00" }, { "name": "symfony/mime", - "version": "v7.4.0", + "version": "v7.4.5", "source": { "type": "git", "url": "https://github.com/symfony/mime.git", - "reference": "bdb02729471be5d047a3ac4a69068748f1a6be7a" + "reference": "b18c7e6e9eee1e19958138df10412f3c4c316148" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/mime/zipball/bdb02729471be5d047a3ac4a69068748f1a6be7a", - "reference": "bdb02729471be5d047a3ac4a69068748f1a6be7a", + "url": "https://api.github.com/repos/symfony/mime/zipball/b18c7e6e9eee1e19958138df10412f3c4c316148", + "reference": "b18c7e6e9eee1e19958138df10412f3c4c316148", "shasum": "" }, "require": { @@ -6513,15 +6513,15 @@ }, "conflict": { "egulias/email-validator": "~3.0.0", - "phpdocumentor/reflection-docblock": "<3.2.2", - "phpdocumentor/type-resolver": "<1.4.0", + "phpdocumentor/reflection-docblock": "<5.2|>=6", + "phpdocumentor/type-resolver": "<1.5.1", "symfony/mailer": "<6.4", "symfony/serializer": "<6.4.3|>7.0,<7.0.3" }, "require-dev": { "egulias/email-validator": "^2.1.10|^3.1|^4", "league/html-to-markdown": "^5.0", - "phpdocumentor/reflection-docblock": "^3.0|^4.0|^5.0", + "phpdocumentor/reflection-docblock": "^5.2", "symfony/dependency-injection": "^6.4|^7.0|^8.0", "symfony/process": "^6.4|^7.0|^8.0", "symfony/property-access": "^6.4|^7.0|^8.0", @@ -6558,7 +6558,7 @@ "mime-type" ], "support": { - "source": "https://github.com/symfony/mime/tree/v7.4.0" + "source": "https://github.com/symfony/mime/tree/v7.4.5" }, "funding": [ { @@ -6578,7 +6578,7 @@ "type": "tidelift" } ], - "time": "2025-11-16T10:14:42+00:00" + "time": "2026-01-27T08:59:58+00:00" }, { "name": "symfony/polyfill-ctype", @@ -7411,16 +7411,16 @@ }, { "name": "symfony/process", - "version": "v7.4.3", + "version": "v7.4.5", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "2f8e1a6cdf590ca63715da4d3a7a3327404a523f" + "reference": "608476f4604102976d687c483ac63a79ba18cc97" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/2f8e1a6cdf590ca63715da4d3a7a3327404a523f", - "reference": "2f8e1a6cdf590ca63715da4d3a7a3327404a523f", + "url": "https://api.github.com/repos/symfony/process/zipball/608476f4604102976d687c483ac63a79ba18cc97", + "reference": "608476f4604102976d687c483ac63a79ba18cc97", "shasum": "" }, "require": { @@ -7452,7 +7452,7 @@ "description": "Executes commands in sub-processes", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/process/tree/v7.4.3" + "source": "https://github.com/symfony/process/tree/v7.4.5" }, "funding": [ { @@ -7472,20 +7472,20 @@ "type": "tidelift" } ], - "time": "2025-12-19T10:00:43+00:00" + "time": "2026-01-26T15:07:59+00:00" }, { "name": "symfony/routing", - "version": "v7.4.3", + "version": "v7.4.4", "source": { "type": "git", "url": "https://github.com/symfony/routing.git", - "reference": "5d3fd7adf8896c2fdb54e2f0f35b1bcbd9e45090" + "reference": "0798827fe2c79caeed41d70b680c2c3507d10147" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/routing/zipball/5d3fd7adf8896c2fdb54e2f0f35b1bcbd9e45090", - "reference": "5d3fd7adf8896c2fdb54e2f0f35b1bcbd9e45090", + "url": "https://api.github.com/repos/symfony/routing/zipball/0798827fe2c79caeed41d70b680c2c3507d10147", + "reference": "0798827fe2c79caeed41d70b680c2c3507d10147", "shasum": "" }, "require": { @@ -7537,7 +7537,7 @@ "url" ], "support": { - "source": "https://github.com/symfony/routing/tree/v7.4.3" + "source": "https://github.com/symfony/routing/tree/v7.4.4" }, "funding": [ { @@ -7557,7 +7557,7 @@ "type": "tidelift" } ], - "time": "2025-12-19T10:00:43+00:00" + "time": "2026-01-12T12:19:02+00:00" }, { "name": "symfony/service-contracts", @@ -7648,16 +7648,16 @@ }, { "name": "symfony/string", - "version": "v8.0.1", + "version": "v8.0.4", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "ba65a969ac918ce0cc3edfac6cdde847eba231dc" + "reference": "758b372d6882506821ed666032e43020c4f57194" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/ba65a969ac918ce0cc3edfac6cdde847eba231dc", - "reference": "ba65a969ac918ce0cc3edfac6cdde847eba231dc", + "url": "https://api.github.com/repos/symfony/string/zipball/758b372d6882506821ed666032e43020c4f57194", + "reference": "758b372d6882506821ed666032e43020c4f57194", "shasum": "" }, "require": { @@ -7714,7 +7714,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v8.0.1" + "source": "https://github.com/symfony/string/tree/v8.0.4" }, "funding": [ { @@ -7734,20 +7734,20 @@ "type": "tidelift" } ], - "time": "2025-12-01T09:13:36+00:00" + "time": "2026-01-12T12:37:40+00:00" }, { "name": "symfony/translation", - "version": "v8.0.3", + "version": "v8.0.4", "source": { "type": "git", "url": "https://github.com/symfony/translation.git", - "reference": "60a8f11f0e15c48f2cc47c4da53873bb5b62135d" + "reference": "db70c8ce7db74fd2da7b1d268db46b2a8ce32c10" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/translation/zipball/60a8f11f0e15c48f2cc47c4da53873bb5b62135d", - "reference": "60a8f11f0e15c48f2cc47c4da53873bb5b62135d", + "url": "https://api.github.com/repos/symfony/translation/zipball/db70c8ce7db74fd2da7b1d268db46b2a8ce32c10", + "reference": "db70c8ce7db74fd2da7b1d268db46b2a8ce32c10", "shasum": "" }, "require": { @@ -7807,7 +7807,7 @@ "description": "Provides tools to internationalize your application", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/translation/tree/v8.0.3" + "source": "https://github.com/symfony/translation/tree/v8.0.4" }, "funding": [ { @@ -7827,7 +7827,7 @@ "type": "tidelift" } ], - "time": "2025-12-21T10:59:45+00:00" + "time": "2026-01-13T13:06:50+00:00" }, { "name": "symfony/translation-contracts", @@ -7913,16 +7913,16 @@ }, { "name": "symfony/uid", - "version": "v7.4.0", + "version": "v7.4.4", "source": { "type": "git", "url": "https://github.com/symfony/uid.git", - "reference": "2498e9f81b7baa206f44de583f2f48350b90142c" + "reference": "7719ce8aba76be93dfe249192f1fbfa52c588e36" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/uid/zipball/2498e9f81b7baa206f44de583f2f48350b90142c", - "reference": "2498e9f81b7baa206f44de583f2f48350b90142c", + "url": "https://api.github.com/repos/symfony/uid/zipball/7719ce8aba76be93dfe249192f1fbfa52c588e36", + "reference": "7719ce8aba76be93dfe249192f1fbfa52c588e36", "shasum": "" }, "require": { @@ -7967,7 +7967,7 @@ "uuid" ], "support": { - "source": "https://github.com/symfony/uid/tree/v7.4.0" + "source": "https://github.com/symfony/uid/tree/v7.4.4" }, "funding": [ { @@ -7987,20 +7987,20 @@ "type": "tidelift" } ], - "time": "2025-09-25T11:02:55+00:00" + "time": "2026-01-03T23:30:35+00:00" }, { "name": "symfony/var-dumper", - "version": "v7.4.3", + "version": "v7.4.4", "source": { "type": "git", "url": "https://github.com/symfony/var-dumper.git", - "reference": "7e99bebcb3f90d8721890f2963463280848cba92" + "reference": "0e4769b46a0c3c62390d124635ce59f66874b282" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-dumper/zipball/7e99bebcb3f90d8721890f2963463280848cba92", - "reference": "7e99bebcb3f90d8721890f2963463280848cba92", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/0e4769b46a0c3c62390d124635ce59f66874b282", + "reference": "0e4769b46a0c3c62390d124635ce59f66874b282", "shasum": "" }, "require": { @@ -8054,7 +8054,7 @@ "dump" ], "support": { - "source": "https://github.com/symfony/var-dumper/tree/v7.4.3" + "source": "https://github.com/symfony/var-dumper/tree/v7.4.4" }, "funding": [ { @@ -8074,7 +8074,7 @@ "type": "tidelift" } ], - "time": "2025-12-18T07:04:31+00:00" + "time": "2026-01-01T22:13:48+00:00" }, { "name": "symfony/var-exporter", @@ -9066,33 +9066,33 @@ }, { "name": "laravel/boost", - "version": "v1.8.10", + "version": "v2.0.3", "source": { "type": "git", "url": "https://github.com/laravel/boost.git", - "reference": "aad8b2a423b0a886c2ce7ee92abbfde69992ff32" + "reference": "e9cd3e8219c49459b2e3f2783292e3db42e9b4d2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/boost/zipball/aad8b2a423b0a886c2ce7ee92abbfde69992ff32", - "reference": "aad8b2a423b0a886c2ce7ee92abbfde69992ff32", + "url": "https://api.github.com/repos/laravel/boost/zipball/e9cd3e8219c49459b2e3f2783292e3db42e9b4d2", + "reference": "e9cd3e8219c49459b2e3f2783292e3db42e9b4d2", "shasum": "" }, "require": { "guzzlehttp/guzzle": "^7.9", - "illuminate/console": "^10.49.0|^11.45.3|^12.41.1", - "illuminate/contracts": "^10.49.0|^11.45.3|^12.41.1", - "illuminate/routing": "^10.49.0|^11.45.3|^12.41.1", - "illuminate/support": "^10.49.0|^11.45.3|^12.41.1", + "illuminate/console": "^11.45.3|^12.41.1", + "illuminate/contracts": "^11.45.3|^12.41.1", + "illuminate/routing": "^11.45.3|^12.41.1", + "illuminate/support": "^11.45.3|^12.41.1", "laravel/mcp": "^0.5.1", - "laravel/prompts": "0.1.25|^0.3.6", + "laravel/prompts": "^0.3.10", "laravel/roster": "^0.2.9", - "php": "^8.1" + "php": "^8.2" }, "require-dev": { - "laravel/pint": "^1.20.0", + "laravel/pint": "^1.27.0", "mockery/mockery": "^1.6.12", - "orchestra/testbench": "^8.36.0|^9.15.0|^10.6", + "orchestra/testbench": "^9.15.0|^10.6", "pestphp/pest": "^2.36.0|^3.8.4|^4.1.5", "phpstan/phpstan": "^2.1.27", "rector/rector": "^2.1" @@ -9128,20 +9128,20 @@ "issues": "https://github.com/laravel/boost/issues", "source": "https://github.com/laravel/boost" }, - "time": "2026-01-14T14:51:16+00:00" + "time": "2026-01-28T08:41:55+00:00" }, { "name": "laravel/mcp", - "version": "v0.5.2", + "version": "v0.5.3", "source": { "type": "git", "url": "https://github.com/laravel/mcp.git", - "reference": "b9bdd8d6f8b547c8733fe6826b1819341597ba3c" + "reference": "39b9791b989927642137dd5b55dde0529f1614f9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/mcp/zipball/b9bdd8d6f8b547c8733fe6826b1819341597ba3c", - "reference": "b9bdd8d6f8b547c8733fe6826b1819341597ba3c", + "url": "https://api.github.com/repos/laravel/mcp/zipball/39b9791b989927642137dd5b55dde0529f1614f9", + "reference": "39b9791b989927642137dd5b55dde0529f1614f9", "shasum": "" }, "require": { @@ -9201,7 +9201,7 @@ "issues": "https://github.com/laravel/mcp/issues", "source": "https://github.com/laravel/mcp" }, - "time": "2025-12-19T19:32:34+00:00" + "time": "2026-01-26T10:25:21+00:00" }, { "name": "laravel/pail", @@ -9717,20 +9717,20 @@ }, { "name": "pestphp/pest", - "version": "v4.3.1", + "version": "v4.3.2", "source": { "type": "git", "url": "https://github.com/pestphp/pest.git", - "reference": "bc57a84e77afd4544ff9643a6858f68d05aeab96" + "reference": "3a4329ddc7a2b67c19fca8342a668b39be3ae398" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/pestphp/pest/zipball/bc57a84e77afd4544ff9643a6858f68d05aeab96", - "reference": "bc57a84e77afd4544ff9643a6858f68d05aeab96", + "url": "https://api.github.com/repos/pestphp/pest/zipball/3a4329ddc7a2b67c19fca8342a668b39be3ae398", + "reference": "3a4329ddc7a2b67c19fca8342a668b39be3ae398", "shasum": "" }, "require": { - "brianium/paratest": "^7.16.0", + "brianium/paratest": "^7.16.1", "nunomaduro/collision": "^8.8.3", "nunomaduro/termwind": "^2.3.3", "pestphp/pest-plugin": "^4.0.0", @@ -9738,18 +9738,18 @@ "pestphp/pest-plugin-mutate": "^4.0.1", "pestphp/pest-plugin-profanity": "^4.2.1", "php": "^8.3.0", - "phpunit/phpunit": "^12.5.4", - "symfony/process": "^7.4.3|^8.0.0" + "phpunit/phpunit": "^12.5.8", + "symfony/process": "^7.4.4|^8.0.0" }, "conflict": { "filp/whoops": "<2.18.3", - "phpunit/phpunit": ">12.5.4", + "phpunit/phpunit": ">12.5.8", "sebastian/exporter": "<7.0.0", "webmozart/assert": "<1.11.0" }, "require-dev": { "pestphp/pest-dev-tools": "^4.0.0", - "pestphp/pest-plugin-browser": "^4.1.1", + "pestphp/pest-plugin-browser": "^4.2.1", "pestphp/pest-plugin-type-coverage": "^4.0.3", "psy/psysh": "^0.12.18" }, @@ -9817,7 +9817,7 @@ ], "support": { "issues": "https://github.com/pestphp/pest/issues", - "source": "https://github.com/pestphp/pest/tree/v4.3.1" + "source": "https://github.com/pestphp/pest/tree/v4.3.2" }, "funding": [ { @@ -9829,7 +9829,7 @@ "type": "github" } ], - "time": "2026-01-04T16:29:59+00:00" + "time": "2026-01-28T01:01:19+00:00" }, { "name": "pestphp/pest-plugin", @@ -10430,11 +10430,11 @@ }, { "name": "phpstan/phpstan", - "version": "2.1.36", + "version": "2.1.37", "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/2132e5e2361d11d40af4c17faa16f043269a4cf3", - "reference": "2132e5e2361d11d40af4c17faa16f043269a4cf3", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/28cd424c5ea984128c95cfa7ea658808e8954e49", + "reference": "28cd424c5ea984128c95cfa7ea658808e8954e49", "shasum": "" }, "require": { @@ -10479,7 +10479,7 @@ "type": "github" } ], - "time": "2026-01-21T13:58:26+00:00" + "time": "2026-01-24T08:21:55+00:00" }, { "name": "phpunit/php-code-coverage", @@ -10817,16 +10817,16 @@ }, { "name": "phpunit/phpunit", - "version": "12.5.4", + "version": "12.5.8", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "4ba0e923f9d3fc655de22f9547c01d15a41fc93a" + "reference": "37ddb96c14bfee10304825edbb7e66d341ec6889" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/4ba0e923f9d3fc655de22f9547c01d15a41fc93a", - "reference": "4ba0e923f9d3fc655de22f9547c01d15a41fc93a", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/37ddb96c14bfee10304825edbb7e66d341ec6889", + "reference": "37ddb96c14bfee10304825edbb7e66d341ec6889", "shasum": "" }, "require": { @@ -10840,13 +10840,13 @@ "phar-io/manifest": "^2.0.4", "phar-io/version": "^3.2.1", "php": ">=8.3", - "phpunit/php-code-coverage": "^12.5.1", + "phpunit/php-code-coverage": "^12.5.2", "phpunit/php-file-iterator": "^6.0.0", "phpunit/php-invoker": "^6.0.0", "phpunit/php-text-template": "^5.0.0", "phpunit/php-timer": "^8.0.0", "sebastian/cli-parser": "^4.2.0", - "sebastian/comparator": "^7.1.3", + "sebastian/comparator": "^7.1.4", "sebastian/diff": "^7.0.0", "sebastian/environment": "^8.0.3", "sebastian/exporter": "^7.0.2", @@ -10894,7 +10894,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/12.5.4" + "source": "https://github.com/sebastianbergmann/phpunit/tree/12.5.8" }, "funding": [ { @@ -10918,7 +10918,7 @@ "type": "tidelift" } ], - "time": "2025-12-15T06:05:34+00:00" + "time": "2026-01-27T06:12:29+00:00" }, { "name": "rector/rector", @@ -11051,16 +11051,16 @@ }, { "name": "sebastian/comparator", - "version": "7.1.3", + "version": "7.1.4", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/comparator.git", - "reference": "dc904b4bb3ab070865fa4068cd84f3da8b945148" + "reference": "6a7de5df2e094f9a80b40a522391a7e6022df5f6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/dc904b4bb3ab070865fa4068cd84f3da8b945148", - "reference": "dc904b4bb3ab070865fa4068cd84f3da8b945148", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/6a7de5df2e094f9a80b40a522391a7e6022df5f6", + "reference": "6a7de5df2e094f9a80b40a522391a7e6022df5f6", "shasum": "" }, "require": { @@ -11119,7 +11119,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/comparator/issues", "security": "https://github.com/sebastianbergmann/comparator/security/policy", - "source": "https://github.com/sebastianbergmann/comparator/tree/7.1.3" + "source": "https://github.com/sebastianbergmann/comparator/tree/7.1.4" }, "funding": [ { @@ -11139,7 +11139,7 @@ "type": "tidelift" } ], - "time": "2025-08-20T11:27:00+00:00" + "time": "2026-01-24T09:28:48+00:00" }, { "name": "sebastian/complexity", diff --git a/package-lock.json b/package-lock.json index e722432..92ade6a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -34,12 +34,12 @@ } }, "node_modules/@babel/code-frame": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", - "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.28.6.tgz", + "integrity": "sha512-JYgintcMjRiCvS8mMECzaEn+m3PfoQiyqukOMCCVQtoJGYJw8j/8LBJEiqkHLkfwCcs74E3pbAUFNg7d9VNJ+Q==", "license": "MIT", "dependencies": { - "@babel/helper-validator-identifier": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" }, @@ -69,9 +69,9 @@ } }, "node_modules/@codemirror/commands": { - "version": "6.10.0", - "resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.10.0.tgz", - "integrity": "sha512-2xUIc5mHXQzT16JnyOFkh8PvfeXuIut3pslWGfsGOhxP/lpgRm9HOl/mpzLErgt5mXDovqA0d11P21gofRLb9w==", + "version": "6.10.1", + "resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.10.1.tgz", + "integrity": "sha512-uWDWFypNdQmz2y1LaNJzK7fL7TYKLeUAU0npEC685OKTF3KcQ2Vu3klIM78D7I6wGhktme0lh3CuQLv0ZCrD9Q==", "license": "MIT", "dependencies": { "@codemirror/language": "^6.0.0", @@ -136,9 +136,9 @@ } }, "node_modules/@codemirror/lang-liquid": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/@codemirror/lang-liquid/-/lang-liquid-6.3.0.tgz", - "integrity": "sha512-fY1YsUExcieXRTsCiwX/bQ9+PbCTA/Fumv7C7mTUZHoFkibfESnaXwpr2aKH6zZVwysEunsHHkaIpM/pl3xETQ==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/@codemirror/lang-liquid/-/lang-liquid-6.3.1.tgz", + "integrity": "sha512-S/jE/D7iij2Pu70AC65ME6AYWxOOcX20cSJvaPgY5w7m2sfxsArAcUAuUgm/CZCVmqoi9KiOlS7gj/gyLipABw==", "license": "MIT", "dependencies": { "@codemirror/autocomplete": "^6.0.0", @@ -152,23 +152,23 @@ } }, "node_modules/@codemirror/language": { - "version": "6.11.3", - "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.11.3.tgz", - "integrity": "sha512-9HBM2XnwDj7fnu0551HkGdrUrrqmYq/WC5iv6nbY2WdicXdGbhR/gfbZOH73Aqj4351alY1+aoG9rCNfiwS1RA==", + "version": "6.12.1", + "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.12.1.tgz", + "integrity": "sha512-Fa6xkSiuGKc8XC8Cn96T+TQHYj4ZZ7RdFmXA3i9xe/3hLHfwPZdM+dqfX0Cp0zQklBKhVD8Yzc8LS45rkqcwpQ==", "license": "MIT", "dependencies": { "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.23.0", - "@lezer/common": "^1.1.0", + "@lezer/common": "^1.5.0", "@lezer/highlight": "^1.0.0", "@lezer/lr": "^1.0.0", "style-mod": "^4.0.0" } }, "node_modules/@codemirror/lint": { - "version": "6.9.2", - "resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.9.2.tgz", - "integrity": "sha512-sv3DylBiIyi+xKwRCJAAsBZZZWo82shJ/RTMymLabAdtbkV5cSKwWDeCgtUq3v8flTaXS2y1kKkICuRYtUswyQ==", + "version": "6.9.3", + "resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.9.3.tgz", + "integrity": "sha512-y3YkYhdnhjDBAe0VIA0c4wVoFOvnp8CnAvfLqi0TqotIv92wIlAAP7HELOpLBsKwjAX6W92rSflA6an/2zBvXw==", "license": "MIT", "dependencies": { "@codemirror/state": "^6.0.0", @@ -177,20 +177,20 @@ } }, "node_modules/@codemirror/search": { - "version": "6.5.11", - "resolved": "https://registry.npmjs.org/@codemirror/search/-/search-6.5.11.tgz", - "integrity": "sha512-KmWepDE6jUdL6n8cAAqIpRmLPBZ5ZKnicE8oGU/s3QrAVID+0VhLFrzUucVKHG5035/BSykhExDL/Xm7dHthiA==", + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/@codemirror/search/-/search-6.6.0.tgz", + "integrity": "sha512-koFuNXcDvyyotWcgOnZGmY7LZqEOXZaaxD/j6n18TCLx2/9HieZJ5H6hs1g8FiRxBD0DNfs0nXn17g872RmYdw==", "license": "MIT", "dependencies": { "@codemirror/state": "^6.0.0", - "@codemirror/view": "^6.0.0", + "@codemirror/view": "^6.37.0", "crelt": "^1.0.5" } }, "node_modules/@codemirror/state": { - "version": "6.5.2", - "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.5.2.tgz", - "integrity": "sha512-FVqsPqtPWKVVL3dPSxy8wEF/ymIEuVzF1PK3VbUgrxXpJUSHQWWZz4JMToquRxnkw+36LTamCZG2iua2Ptq0fA==", + "version": "6.5.4", + "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.5.4.tgz", + "integrity": "sha512-8y7xqG/hpB53l25CIoit9/ngxdfoG+fx+V3SHBrinnhOtLvKHRyAJJuHzkWrR4YXXLX8eXBsejgAAxHUOdW1yw==", "license": "MIT", "dependencies": { "@marijn/find-cluster-break": "^1.0.0" @@ -209,9 +209,9 @@ } }, "node_modules/@codemirror/view": { - "version": "6.38.8", - "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.38.8.tgz", - "integrity": "sha512-XcE9fcnkHCbWkjeKyi0lllwXmBLtyYb5dt89dJyx23I9+LSh5vZDIuk7OLG4VM1lgrXZQcY6cxyZyk5WVPRv/A==", + "version": "6.39.11", + "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.39.11.tgz", + "integrity": "sha512-bWdeR8gWM87l4DB/kYSF9A+dVackzDb/V56Tq7QVrQ7rn86W0rgZFtlL3g3pem6AeGcb9NQNoy3ao4WpW4h5tQ==", "license": "MIT", "dependencies": { "@codemirror/state": "^6.5.0", @@ -221,9 +221,9 @@ } }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", - "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz", + "integrity": "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==", "cpu": [ "ppc64" ], @@ -237,9 +237,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", - "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.2.tgz", + "integrity": "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==", "cpu": [ "arm" ], @@ -253,9 +253,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", - "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.2.tgz", + "integrity": "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==", "cpu": [ "arm64" ], @@ -269,9 +269,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", - "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.2.tgz", + "integrity": "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==", "cpu": [ "x64" ], @@ -285,9 +285,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", - "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.2.tgz", + "integrity": "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==", "cpu": [ "arm64" ], @@ -301,9 +301,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", - "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.2.tgz", + "integrity": "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==", "cpu": [ "x64" ], @@ -317,9 +317,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", - "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.2.tgz", + "integrity": "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==", "cpu": [ "arm64" ], @@ -333,9 +333,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", - "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.2.tgz", + "integrity": "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==", "cpu": [ "x64" ], @@ -349,9 +349,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", - "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.2.tgz", + "integrity": "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==", "cpu": [ "arm" ], @@ -365,9 +365,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", - "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.2.tgz", + "integrity": "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==", "cpu": [ "arm64" ], @@ -381,9 +381,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", - "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.2.tgz", + "integrity": "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==", "cpu": [ "ia32" ], @@ -397,9 +397,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", - "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.2.tgz", + "integrity": "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==", "cpu": [ "loong64" ], @@ -413,9 +413,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", - "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.2.tgz", + "integrity": "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==", "cpu": [ "mips64el" ], @@ -429,9 +429,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", - "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.2.tgz", + "integrity": "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==", "cpu": [ "ppc64" ], @@ -445,9 +445,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", - "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.2.tgz", + "integrity": "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==", "cpu": [ "riscv64" ], @@ -461,9 +461,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", - "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.2.tgz", + "integrity": "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==", "cpu": [ "s390x" ], @@ -477,9 +477,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", - "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.2.tgz", + "integrity": "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==", "cpu": [ "x64" ], @@ -493,9 +493,9 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", - "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.2.tgz", + "integrity": "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==", "cpu": [ "arm64" ], @@ -509,9 +509,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", - "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.2.tgz", + "integrity": "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==", "cpu": [ "x64" ], @@ -525,9 +525,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", - "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.2.tgz", + "integrity": "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==", "cpu": [ "arm64" ], @@ -541,9 +541,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", - "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.2.tgz", + "integrity": "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==", "cpu": [ "x64" ], @@ -557,9 +557,9 @@ } }, "node_modules/@esbuild/openharmony-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", - "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.2.tgz", + "integrity": "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==", "cpu": [ "arm64" ], @@ -573,9 +573,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", - "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.2.tgz", + "integrity": "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==", "cpu": [ "x64" ], @@ -589,9 +589,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", - "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.2.tgz", + "integrity": "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==", "cpu": [ "arm64" ], @@ -605,9 +605,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", - "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.2.tgz", + "integrity": "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==", "cpu": [ "ia32" ], @@ -621,9 +621,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", - "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.2.tgz", + "integrity": "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==", "cpu": [ "x64" ], @@ -694,9 +694,9 @@ } }, "node_modules/@lezer/common": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.4.0.tgz", - "integrity": "sha512-DVeMRoGrgn/k45oQNu189BoW4SZwgZFzJ1+1TV5j2NJ/KFC83oa/enRqZSGshyeMk5cPWMhsKs9nx+8o0unwGg==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.5.0.tgz", + "integrity": "sha512-PNGcolp9hr4PJdXR4ix7XtixDrClScvtSCYW3rQG106oVMOOI+jFb+0+J3mbeL/53g1Zd6s0kJzaw6Ri68GmAA==", "license": "MIT" }, "node_modules/@lezer/css": { @@ -720,9 +720,9 @@ } }, "node_modules/@lezer/html": { - "version": "1.3.12", - "resolved": "https://registry.npmjs.org/@lezer/html/-/html-1.3.12.tgz", - "integrity": "sha512-RJ7eRWdaJe3bsiiLLHjCFT1JMk8m1YP9kaUbvu2rMLEoOnke9mcTVDyfOslsln0LtujdWespjJ39w6zo+RsQYw==", + "version": "1.3.13", + "resolved": "https://registry.npmjs.org/@lezer/html/-/html-1.3.13.tgz", + "integrity": "sha512-oI7n6NJml729m7pjm9lvLvmXbdoMoi2f+1pwSDJkl9d68zGr7a9Btz8NdHTGQZtW2DA25ybeuv/SyDb9D5tseg==", "license": "MIT", "dependencies": { "@lezer/common": "^1.2.0", @@ -753,9 +753,9 @@ } }, "node_modules/@lezer/lr": { - "version": "1.4.4", - "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.4.tgz", - "integrity": "sha512-LHL17Mq0OcFXm1pGQssuGTQFPPdxARjKM8f7GA5+sGtHi0K3R84YaSbmche0+RKWHnCsx9asEe5OWOI4FHfe4A==", + "version": "1.4.8", + "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.8.tgz", + "integrity": "sha512-bPWa0Pgx69ylNlMlPvBPryqeLYQjyJjqPx+Aupm5zydLIF3NE+6MMLT8Yi23Bd9cif9VS00aUebn+6fDIGBcDA==", "license": "MIT", "dependencies": { "@lezer/common": "^1.0.0" @@ -789,9 +789,9 @@ } }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.53.3.tgz", - "integrity": "sha512-mRSi+4cBjrRLoaal2PnqH82Wqyb+d3HsPUN/W+WslCXsZsyHa9ZeQQX/pQsZaVIWDkPcpV6jJ+3KLbTbgnwv8w==", + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.57.0.tgz", + "integrity": "sha512-tPgXB6cDTndIe1ah7u6amCI1T0SsnlOuKgg10Xh3uizJk4e5M1JGaUMk7J4ciuAUcFpbOiNhm2XIjP9ON0dUqA==", "cpu": [ "arm" ], @@ -802,9 +802,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.53.3.tgz", - "integrity": "sha512-CbDGaMpdE9sh7sCmTrTUyllhrg65t6SwhjlMJsLr+J8YjFuPmCEjbBSx4Z/e4SmDyH3aB5hGaJUP2ltV/vcs4w==", + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.57.0.tgz", + "integrity": "sha512-sa4LyseLLXr1onr97StkU1Nb7fWcg6niokTwEVNOO7awaKaoRObQ54+V/hrF/BP1noMEaaAW6Fg2d/CfLiq3Mg==", "cpu": [ "arm64" ], @@ -815,9 +815,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.53.3.tgz", - "integrity": "sha512-Nr7SlQeqIBpOV6BHHGZgYBuSdanCXuw09hon14MGOLGmXAFYjx1wNvquVPmpZnl0tLjg25dEdr4IQ6GgyToCUA==", + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.57.0.tgz", + "integrity": "sha512-/NNIj9A7yLjKdmkx5dC2XQ9DmjIECpGpwHoGmA5E1AhU0fuICSqSWScPhN1yLCkEdkCwJIDu2xIeLPs60MNIVg==", "cpu": [ "arm64" ], @@ -828,9 +828,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.53.3.tgz", - "integrity": "sha512-DZ8N4CSNfl965CmPktJ8oBnfYr3F8dTTNBQkRlffnUarJ2ohudQD17sZBa097J8xhQ26AwhHJ5mvUyQW8ddTsQ==", + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.57.0.tgz", + "integrity": "sha512-xoh8abqgPrPYPr7pTYipqnUi1V3em56JzE/HgDgitTqZBZ3yKCWI+7KUkceM6tNweyUKYru1UMi7FC060RyKwA==", "cpu": [ "x64" ], @@ -841,9 +841,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.53.3.tgz", - "integrity": "sha512-yMTrCrK92aGyi7GuDNtGn2sNW+Gdb4vErx4t3Gv/Tr+1zRb8ax4z8GWVRfr3Jw8zJWvpGHNpss3vVlbF58DZ4w==", + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.57.0.tgz", + "integrity": "sha512-PCkMh7fNahWSbA0OTUQ2OpYHpjZZr0hPr8lId8twD7a7SeWrvT3xJVyza+dQwXSSq4yEQTMoXgNOfMCsn8584g==", "cpu": [ "arm64" ], @@ -854,9 +854,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.53.3.tgz", - "integrity": "sha512-lMfF8X7QhdQzseM6XaX0vbno2m3hlyZFhwcndRMw8fbAGUGL3WFMBdK0hbUBIUYcEcMhVLr1SIamDeuLBnXS+Q==", + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.57.0.tgz", + "integrity": "sha512-1j3stGx+qbhXql4OCDZhnK7b01s6rBKNybfsX+TNrEe9JNq4DLi1yGiR1xW+nL+FNVvI4D02PUnl6gJ/2y6WJA==", "cpu": [ "x64" ], @@ -867,9 +867,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.53.3.tgz", - "integrity": "sha512-k9oD15soC/Ln6d2Wv/JOFPzZXIAIFLp6B+i14KhxAfnq76ajt0EhYc5YPeX6W1xJkAdItcVT+JhKl1QZh44/qw==", + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.57.0.tgz", + "integrity": "sha512-eyrr5W08Ms9uM0mLcKfM/Uzx7hjhz2bcjv8P2uynfj0yU8GGPdz8iYrBPhiLOZqahoAMB8ZiolRZPbbU2MAi6Q==", "cpu": [ "arm" ], @@ -880,9 +880,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.53.3.tgz", - "integrity": "sha512-vTNlKq+N6CK/8UktsrFuc+/7NlEYVxgaEgRXVUVK258Z5ymho29skzW1sutgYjqNnquGwVUObAaxae8rZ6YMhg==", + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.57.0.tgz", + "integrity": "sha512-Xds90ITXJCNyX9pDhqf85MKWUI4lqjiPAipJ8OLp8xqI2Ehk+TCVhF9rvOoN8xTbcafow3QOThkNnrM33uCFQA==", "cpu": [ "arm" ], @@ -893,9 +893,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.53.3.tgz", - "integrity": "sha512-RGrFLWgMhSxRs/EWJMIFM1O5Mzuz3Xy3/mnxJp/5cVhZ2XoCAxJnmNsEyeMJtpK+wu0FJFWz+QF4mjCA7AUQ3w==", + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.57.0.tgz", + "integrity": "sha512-Xws2KA4CLvZmXjy46SQaXSejuKPhwVdaNinldoYfqruZBaJHqVo6hnRa8SDo9z7PBW5x84SH64+izmldCgbezw==", "cpu": [ "arm64" ], @@ -906,9 +906,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.53.3.tgz", - "integrity": "sha512-kASyvfBEWYPEwe0Qv4nfu6pNkITLTb32p4yTgzFCocHnJLAHs+9LjUu9ONIhvfT/5lv4YS5muBHyuV84epBo/A==", + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.57.0.tgz", + "integrity": "sha512-hrKXKbX5FdaRJj7lTMusmvKbhMJSGWJ+w++4KmjiDhpTgNlhYobMvKfDoIWecy4O60K6yA4SnztGuNTQF+Lplw==", "cpu": [ "arm64" ], @@ -919,9 +919,22 @@ ] }, "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.53.3.tgz", - "integrity": "sha512-JiuKcp2teLJwQ7vkJ95EwESWkNRFJD7TQgYmCnrPtlu50b4XvT5MOmurWNrCj3IFdyjBQ5p9vnrX4JM6I8OE7g==", + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.57.0.tgz", + "integrity": "sha512-6A+nccfSDGKsPm00d3xKcrsBcbqzCTAukjwWK6rbuAnB2bHaL3r9720HBVZ/no7+FhZLz/U3GwwZZEh6tOSI8Q==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.57.0.tgz", + "integrity": "sha512-4P1VyYUe6XAJtQH1Hh99THxr0GKMMwIXsRNOceLrJnaHTDgk1FTcTimDgneRJPvB3LqDQxUmroBclQ1S0cIJwQ==", "cpu": [ "loong64" ], @@ -932,9 +945,22 @@ ] }, "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.53.3.tgz", - "integrity": "sha512-EoGSa8nd6d3T7zLuqdojxC20oBfNT8nexBbB/rkxgKj5T5vhpAQKKnD+h3UkoMuTyXkP5jTjK/ccNRmQrPNDuw==", + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.57.0.tgz", + "integrity": "sha512-8Vv6pLuIZCMcgXre6c3nOPhE0gjz1+nZP6T+hwWjr7sVH8k0jRkH+XnfjjOTglyMBdSKBPPz54/y1gToSKwrSQ==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.57.0.tgz", + "integrity": "sha512-r1te1M0Sm2TBVD/RxBPC6RZVwNqUTwJTA7w+C/IW5v9Ssu6xmxWEi+iJQlpBhtUiT1raJ5b48pI8tBvEjEFnFA==", "cpu": [ "ppc64" ], @@ -945,9 +971,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.53.3.tgz", - "integrity": "sha512-4s+Wped2IHXHPnAEbIB0YWBv7SDohqxobiiPA1FIWZpX+w9o2i4LezzH/NkFUl8LRci/8udci6cLq+jJQlh+0g==", + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.57.0.tgz", + "integrity": "sha512-say0uMU/RaPm3CDQLxUUTF2oNWL8ysvHkAjcCzV2znxBr23kFfaxocS9qJm+NdkRhF8wtdEEAJuYcLPhSPbjuQ==", "cpu": [ "riscv64" ], @@ -958,9 +984,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.53.3.tgz", - "integrity": "sha512-68k2g7+0vs2u9CxDt5ktXTngsxOQkSEV/xBbwlqYcUrAVh6P9EgMZvFsnHy4SEiUl46Xf0IObWVbMvPrr2gw8A==", + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.57.0.tgz", + "integrity": "sha512-/MU7/HizQGsnBREtRpcSbSV1zfkoxSTR7wLsRmBPQ8FwUj5sykrP1MyJTvsxP5KBq9SyE6kH8UQQQwa0ASeoQQ==", "cpu": [ "riscv64" ], @@ -971,9 +997,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.53.3.tgz", - "integrity": "sha512-VYsFMpULAz87ZW6BVYw3I6sWesGpsP9OPcyKe8ofdg9LHxSbRMd7zrVrr5xi/3kMZtpWL/wC+UIJWJYVX5uTKg==", + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.57.0.tgz", + "integrity": "sha512-Q9eh+gUGILIHEaJf66aF6a414jQbDnn29zeu0eX3dHMuysnhTvsUvZTCAyZ6tJhUjnvzBKE4FtuaYxutxRZpOg==", "cpu": [ "s390x" ], @@ -997,9 +1023,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.53.3.tgz", - "integrity": "sha512-eoROhjcc6HbZCJr+tvVT8X4fW3/5g/WkGvvmwz/88sDtSJzO7r/blvoBDgISDiCjDRZmHpwud7h+6Q9JxFwq1Q==", + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.57.0.tgz", + "integrity": "sha512-XeatKzo4lHDsVEbm1XDHZlhYZZSQYym6dg2X/Ko0kSFgio+KXLsxwJQprnR48GvdIKDOpqWqssC3iBCjoMcMpw==", "cpu": [ "x64" ], @@ -1009,10 +1035,23 @@ "linux" ] }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.57.0.tgz", + "integrity": "sha512-Lu71y78F5qOfYmubYLHPcJm74GZLU6UJ4THkf/a1K7Tz2ycwC2VUbsqbJAXaR6Bx70SRdlVrt2+n5l7F0agTUw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.53.3.tgz", - "integrity": "sha512-OueLAWgrNSPGAdUdIjSWXw+u/02BRTcnfw9PN41D2vq/JSEPnJnVuBgw18VkN8wcd4fjUs+jFHVM4t9+kBSNLw==", + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.57.0.tgz", + "integrity": "sha512-v5xwKDWcu7qhAEcsUubiav7r+48Uk/ENWdr82MBZZRIm7zThSxCIVDfb3ZeRRq9yqk+oIzMdDo6fCcA5DHfMyA==", "cpu": [ "arm64" ], @@ -1023,9 +1062,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.53.3.tgz", - "integrity": "sha512-GOFuKpsxR/whszbF/bzydebLiXIHSgsEUp6M0JI8dWvi+fFa1TD6YQa4aSZHtpmh2/uAlj/Dy+nmby3TJ3pkTw==", + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.57.0.tgz", + "integrity": "sha512-XnaaaSMGSI6Wk8F4KK3QP7GfuuhjGchElsVerCplUuxRIzdvZ7hRBpLR0omCmw+kI2RFJB80nenhOoGXlJ5TfQ==", "cpu": [ "arm64" ], @@ -1036,9 +1075,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.53.3.tgz", - "integrity": "sha512-iah+THLcBJdpfZ1TstDFbKNznlzoxa8fmnFYK4V67HvmuNYkVdAywJSoteUszvBQ9/HqN2+9AZghbajMsFT+oA==", + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.57.0.tgz", + "integrity": "sha512-3K1lP+3BXY4t4VihLw5MEg6IZD3ojSYzqzBG571W3kNQe4G4CcFpSUQVgurYgib5d+YaCjeFow8QivWp8vuSvA==", "cpu": [ "ia32" ], @@ -1049,9 +1088,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.53.3.tgz", - "integrity": "sha512-J9QDiOIZlZLdcot5NXEepDkstocktoVjkaKUtqzgzpt2yWjGlbYiKyp05rWwk4nypbYUNoFAztEgixoLaSETkg==", + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.57.0.tgz", + "integrity": "sha512-MDk610P/vJGc5L5ImE4k5s+GZT3en0KoK1MKPXCRgzmksAMk79j4h3k1IerxTNqwDLxsGxStEZVBqG0gIqZqoA==", "cpu": [ "x64" ], @@ -1062,9 +1101,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.53.3.tgz", - "integrity": "sha512-UhTd8u31dXadv0MopwGgNOBpUVROFKWVQgAg5N1ESyCz8AuBcMqm4AuTjrwgQKGDfoFuz02EuMRHQIw/frmYKQ==", + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.57.0.tgz", + "integrity": "sha512-Zv7v6q6aV+VslnpwzqKAmrk5JdVkLUzok2208ZXGipjb+msxBr/fJPZyeEXiFgH7k62Ak0SLIfxQRZQvTuf7rQ==", "cpu": [ "x64" ], @@ -1075,9 +1114,9 @@ ] }, "node_modules/@tailwindcss/node": { - "version": "4.1.17", - "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.17.tgz", - "integrity": "sha512-csIkHIgLb3JisEFQ0vxr2Y57GUNYh447C8xzwj89U/8fdW8LhProdxvnVH6U8M2Y73QKiTIH+LWbK3V2BBZsAg==", + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.18.tgz", + "integrity": "sha512-DoR7U1P7iYhw16qJ49fgXUlry1t4CpXeErJHnQ44JgTSKMaZUdf17cfn5mHchfJ4KRBZRFA/Coo+MUF5+gOaCQ==", "license": "MIT", "dependencies": { "@jridgewell/remapping": "^2.3.4", @@ -1086,36 +1125,36 @@ "lightningcss": "1.30.2", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", - "tailwindcss": "4.1.17" + "tailwindcss": "4.1.18" } }, "node_modules/@tailwindcss/oxide": { - "version": "4.1.17", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.17.tgz", - "integrity": "sha512-F0F7d01fmkQhsTjXezGBLdrl1KresJTcI3DB8EkScCldyKp3Msz4hub4uyYaVnk88BAS1g5DQjjF6F5qczheLA==", + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.18.tgz", + "integrity": "sha512-EgCR5tTS5bUSKQgzeMClT6iCY3ToqE1y+ZB0AKldj809QXk1Y+3jB0upOYZrn9aGIzPtUsP7sX4QQ4XtjBB95A==", "license": "MIT", "engines": { "node": ">= 10" }, "optionalDependencies": { - "@tailwindcss/oxide-android-arm64": "4.1.17", - "@tailwindcss/oxide-darwin-arm64": "4.1.17", - "@tailwindcss/oxide-darwin-x64": "4.1.17", - "@tailwindcss/oxide-freebsd-x64": "4.1.17", - "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.17", - "@tailwindcss/oxide-linux-arm64-gnu": "4.1.17", - "@tailwindcss/oxide-linux-arm64-musl": "4.1.17", - "@tailwindcss/oxide-linux-x64-gnu": "4.1.17", - "@tailwindcss/oxide-linux-x64-musl": "4.1.17", - "@tailwindcss/oxide-wasm32-wasi": "4.1.17", - "@tailwindcss/oxide-win32-arm64-msvc": "4.1.17", - "@tailwindcss/oxide-win32-x64-msvc": "4.1.17" + "@tailwindcss/oxide-android-arm64": "4.1.18", + "@tailwindcss/oxide-darwin-arm64": "4.1.18", + "@tailwindcss/oxide-darwin-x64": "4.1.18", + "@tailwindcss/oxide-freebsd-x64": "4.1.18", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.18", + "@tailwindcss/oxide-linux-arm64-gnu": "4.1.18", + "@tailwindcss/oxide-linux-arm64-musl": "4.1.18", + "@tailwindcss/oxide-linux-x64-gnu": "4.1.18", + "@tailwindcss/oxide-linux-x64-musl": "4.1.18", + "@tailwindcss/oxide-wasm32-wasi": "4.1.18", + "@tailwindcss/oxide-win32-arm64-msvc": "4.1.18", + "@tailwindcss/oxide-win32-x64-msvc": "4.1.18" } }, "node_modules/@tailwindcss/oxide-android-arm64": { - "version": "4.1.17", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.17.tgz", - "integrity": "sha512-BMqpkJHgOZ5z78qqiGE6ZIRExyaHyuxjgrJ6eBO5+hfrfGkuya0lYfw8fRHG77gdTjWkNWEEm+qeG2cDMxArLQ==", + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.18.tgz", + "integrity": "sha512-dJHz7+Ugr9U/diKJA0W6N/6/cjI+ZTAoxPf9Iz9BFRF2GzEX8IvXxFIi/dZBloVJX/MZGvRuFA9rqwdiIEZQ0Q==", "cpu": [ "arm64" ], @@ -1129,9 +1168,9 @@ } }, "node_modules/@tailwindcss/oxide-darwin-arm64": { - "version": "4.1.17", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.17.tgz", - "integrity": "sha512-EquyumkQweUBNk1zGEU/wfZo2qkp/nQKRZM8bUYO0J+Lums5+wl2CcG1f9BgAjn/u9pJzdYddHWBiFXJTcxmOg==", + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.18.tgz", + "integrity": "sha512-Gc2q4Qhs660bhjyBSKgq6BYvwDz4G+BuyJ5H1xfhmDR3D8HnHCmT/BSkvSL0vQLy/nkMLY20PQ2OoYMO15Jd0A==", "cpu": [ "arm64" ], @@ -1145,9 +1184,9 @@ } }, "node_modules/@tailwindcss/oxide-darwin-x64": { - "version": "4.1.17", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.17.tgz", - "integrity": "sha512-gdhEPLzke2Pog8s12oADwYu0IAw04Y2tlmgVzIN0+046ytcgx8uZmCzEg4VcQh+AHKiS7xaL8kGo/QTiNEGRog==", + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.18.tgz", + "integrity": "sha512-FL5oxr2xQsFrc3X9o1fjHKBYBMD1QZNyc1Xzw/h5Qu4XnEBi3dZn96HcHm41c/euGV+GRiXFfh2hUCyKi/e+yw==", "cpu": [ "x64" ], @@ -1161,9 +1200,9 @@ } }, "node_modules/@tailwindcss/oxide-freebsd-x64": { - "version": "4.1.17", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.17.tgz", - "integrity": "sha512-hxGS81KskMxML9DXsaXT1H0DyA+ZBIbyG/sSAjWNe2EDl7TkPOBI42GBV3u38itzGUOmFfCzk1iAjDXds8Oh0g==", + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.18.tgz", + "integrity": "sha512-Fj+RHgu5bDodmV1dM9yAxlfJwkkWvLiRjbhuO2LEtwtlYlBgiAT4x/j5wQr1tC3SANAgD+0YcmWVrj8R9trVMA==", "cpu": [ "x64" ], @@ -1177,9 +1216,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { - "version": "4.1.17", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.17.tgz", - "integrity": "sha512-k7jWk5E3ldAdw0cNglhjSgv501u7yrMf8oeZ0cElhxU6Y2o7f8yqelOp3fhf7evjIS6ujTI3U8pKUXV2I4iXHQ==", + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.18.tgz", + "integrity": "sha512-Fp+Wzk/Ws4dZn+LV2Nqx3IilnhH51YZoRaYHQsVq3RQvEl+71VGKFpkfHrLM/Li+kt5c0DJe/bHXK1eHgDmdiA==", "cpu": [ "arm" ], @@ -1193,9 +1232,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { - "version": "4.1.17", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.17.tgz", - "integrity": "sha512-HVDOm/mxK6+TbARwdW17WrgDYEGzmoYayrCgmLEw7FxTPLcp/glBisuyWkFz/jb7ZfiAXAXUACfyItn+nTgsdQ==", + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.18.tgz", + "integrity": "sha512-S0n3jboLysNbh55Vrt7pk9wgpyTTPD0fdQeh7wQfMqLPM/Hrxi+dVsLsPrycQjGKEQk85Kgbx+6+QnYNiHalnw==", "cpu": [ "arm64" ], @@ -1209,9 +1248,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-arm64-musl": { - "version": "4.1.17", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.17.tgz", - "integrity": "sha512-HvZLfGr42i5anKtIeQzxdkw/wPqIbpeZqe7vd3V9vI3RQxe3xU1fLjss0TjyhxWcBaipk7NYwSrwTwK1hJARMg==", + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.18.tgz", + "integrity": "sha512-1px92582HkPQlaaCkdRcio71p8bc8i/ap5807tPRDK/uw953cauQBT8c5tVGkOwrHMfc2Yh6UuxaH4vtTjGvHg==", "cpu": [ "arm64" ], @@ -1225,9 +1264,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-x64-gnu": { - "version": "4.1.17", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.17.tgz", - "integrity": "sha512-M3XZuORCGB7VPOEDH+nzpJ21XPvK5PyjlkSFkFziNHGLc5d6g3di2McAAblmaSUNl8IOmzYwLx9NsE7bplNkwQ==", + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.18.tgz", + "integrity": "sha512-v3gyT0ivkfBLoZGF9LyHmts0Isc8jHZyVcbzio6Wpzifg/+5ZJpDiRiUhDLkcr7f/r38SWNe7ucxmGW3j3Kb/g==", "cpu": [ "x64" ], @@ -1241,9 +1280,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-x64-musl": { - "version": "4.1.17", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.17.tgz", - "integrity": "sha512-k7f+pf9eXLEey4pBlw+8dgfJHY4PZ5qOUFDyNf7SI6lHjQ9Zt7+NcscjpwdCEbYi6FI5c2KDTDWyf2iHcCSyyQ==", + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.18.tgz", + "integrity": "sha512-bhJ2y2OQNlcRwwgOAGMY0xTFStt4/wyU6pvI6LSuZpRgKQwxTec0/3Scu91O8ir7qCR3AuepQKLU/kX99FouqQ==", "cpu": [ "x64" ], @@ -1257,9 +1296,9 @@ } }, "node_modules/@tailwindcss/oxide-wasm32-wasi": { - "version": "4.1.17", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.17.tgz", - "integrity": "sha512-cEytGqSSoy7zK4JRWiTCx43FsKP/zGr0CsuMawhH67ONlH+T79VteQeJQRO/X7L0juEUA8ZyuYikcRBf0vsxhg==", + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.18.tgz", + "integrity": "sha512-LffYTvPjODiP6PT16oNeUQJzNVyJl1cjIebq/rWWBF+3eDst5JGEFSc5cWxyRCJ0Mxl+KyIkqRxk1XPEs9x8TA==", "bundleDependencies": [ "@napi-rs/wasm-runtime", "@emnapi/core", @@ -1274,10 +1313,10 @@ "license": "MIT", "optional": true, "dependencies": { - "@emnapi/core": "^1.6.0", - "@emnapi/runtime": "^1.6.0", + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1", "@emnapi/wasi-threads": "^1.1.0", - "@napi-rs/wasm-runtime": "^1.0.7", + "@napi-rs/wasm-runtime": "^1.1.0", "@tybys/wasm-util": "^0.10.1", "tslib": "^2.4.0" }, @@ -1286,9 +1325,9 @@ } }, "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { - "version": "4.1.17", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.17.tgz", - "integrity": "sha512-JU5AHr7gKbZlOGvMdb4722/0aYbU+tN6lv1kONx0JK2cGsh7g148zVWLM0IKR3NeKLv+L90chBVYcJ8uJWbC9A==", + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.18.tgz", + "integrity": "sha512-HjSA7mr9HmC8fu6bdsZvZ+dhjyGCLdotjVOgLA2vEqxEBZaQo9YTX4kwgEvPCpRh8o4uWc4J/wEoFzhEmjvPbA==", "cpu": [ "arm64" ], @@ -1302,9 +1341,9 @@ } }, "node_modules/@tailwindcss/oxide-win32-x64-msvc": { - "version": "4.1.17", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.17.tgz", - "integrity": "sha512-SKWM4waLuqx0IH+FMDUw6R66Hu4OuTALFgnleKbqhgGU30DY20NORZMZUKgLRjQXNN2TLzKvh48QXTig4h4bGw==", + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.18.tgz", + "integrity": "sha512-bJWbyYpUlqamC8dpR7pfjA0I7vdF6t5VpUGMWRkXVE3AXgIZjYUYAK7II1GNaxR8J1SSrSrppRar8G++JekE3Q==", "cpu": [ "x64" ], @@ -1318,14 +1357,14 @@ } }, "node_modules/@tailwindcss/vite": { - "version": "4.1.17", - "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.1.17.tgz", - "integrity": "sha512-4+9w8ZHOiGnpcGI6z1TVVfWaX/koK7fKeSYF3qlYg2xpBtbteP2ddBxiarL+HVgfSJGeK5RIxRQmKm4rTJJAwA==", + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.1.18.tgz", + "integrity": "sha512-jVA+/UpKL1vRLg6Hkao5jldawNmRo7mQYrZtNHMIVpLfLhDml5nMRUo/8MwoX2vNXvnaXNNMedrMfMugAVX1nA==", "license": "MIT", "dependencies": { - "@tailwindcss/node": "4.1.17", - "@tailwindcss/oxide": "4.1.17", - "tailwindcss": "4.1.17" + "@tailwindcss/node": "4.1.18", + "@tailwindcss/oxide": "4.1.18", + "tailwindcss": "4.1.18" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7" @@ -1344,9 +1383,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "24.10.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.1.tgz", - "integrity": "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==", + "version": "25.0.10", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.0.10.tgz", + "integrity": "sha512-zWW5KPngR/yvakJgGOmZ5vTBemDoSqF3AcV/LrO5u5wTWyEAVVh+IT39G4gtyAkh3CtTZs8aX/yRM82OfzHJRg==", "license": "MIT", "optional": true, "dependencies": { @@ -1421,9 +1460,9 @@ "license": "MIT" }, "node_modules/autoprefixer": { - "version": "10.4.22", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.22.tgz", - "integrity": "sha512-ARe0v/t9gO28Bznv6GgqARmVqcWOV3mfgUPn9becPHMiD3o9BwlRgaeccZnwTpZ7Zwqrm+c1sUSsMxIzQzc8Xg==", + "version": "10.4.23", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.23.tgz", + "integrity": "sha512-YYTXSFulfwytnjAPlw8QHncHJmlvFKtczb8InXaAx9Q0LbfDnfEYDE55omerIJKihhmU61Ft+cAOSzQVaBUmeA==", "funding": [ { "type": "opencollective", @@ -1440,10 +1479,9 @@ ], "license": "MIT", "dependencies": { - "browserslist": "^4.27.0", - "caniuse-lite": "^1.0.30001754", + "browserslist": "^4.28.1", + "caniuse-lite": "^1.0.30001760", "fraction.js": "^5.3.4", - "normalize-range": "^0.1.2", "picocolors": "^1.1.1", "postcss-value-parser": "^4.2.0" }, @@ -1458,9 +1496,9 @@ } }, "node_modules/axios": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz", - "integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==", + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.4.tgz", + "integrity": "sha512-1wVkUaAO6WyaYtCkcYCOx12ZgpGf9Zif+qXa4n+oYzK558YryKqiL6UWwd5DqiH3VRW0GYhTZQ/vlgJrCoNQlg==", "license": "MIT", "dependencies": { "follow-redirects": "^1.15.6", @@ -1497,9 +1535,9 @@ } }, "node_modules/bare-fs": { - "version": "4.5.2", - "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.5.2.tgz", - "integrity": "sha512-veTnRzkb6aPHOvSKIOy60KzURfBdUflr5VReI+NSaPL6xf+XLdONQgZgpYvUuZLVQ8dCqxpBAudaOM1+KpAUxw==", + "version": "4.5.3", + "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.5.3.tgz", + "integrity": "sha512-9+kwVx8QYvt3hPWnmb19tPnh38c6Nihz8Lx3t0g9+4GoIf3/fTgYwM4Z6NxgI+B9elLQA7mLE9PpqcWtOMRDiQ==", "license": "Apache-2.0", "optional": true, "dependencies": { @@ -1574,18 +1612,18 @@ } }, "node_modules/baseline-browser-mapping": { - "version": "2.9.3", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.3.tgz", - "integrity": "sha512-8QdH6czo+G7uBsNo0GiUfouPN1lRzKdJTGnKXwe12gkFbnnOUaUKGN55dMkfy+mnxmvjwl9zcI4VncczcVXDhA==", + "version": "2.9.18", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.18.tgz", + "integrity": "sha512-e23vBV1ZLfjb9apvfPk4rHVu2ry6RIr2Wfs+O324okSidrX7pTAnEJPCh/O5BtRlr7QtZI7ktOP3vsqr7Z5XoA==", "license": "Apache-2.0", "bin": { "baseline-browser-mapping": "dist/cli.js" } }, "node_modules/basic-ftp": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.0.5.tgz", - "integrity": "sha512-4Bcg1P8xhUuqcii/S0Z9wiHIrQVPMermM1any+MX5GeGD7faD3/msQUDGLol9wOcz4/jbg/WJnGqoJF6LiBdtg==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.1.0.tgz", + "integrity": "sha512-RkaJzeJKDbaDWTIPiJwubyljaEPwpVWkm9Rt5h9Nd6h7tEXTJ3VB4qxdZBioV7JO5yLUaOKwz7vDOzlncUsegw==", "license": "MIT", "engines": { "node": ">=10.0.0" @@ -1656,9 +1694,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001759", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001759.tgz", - "integrity": "sha512-Pzfx9fOKoKvevQf8oCXoyNRQ5QyxJj+3O0Rqx2V5oxT61KGx8+n6hV/IUyJeifUci2clnmmKVpvtiqRzgiWjSw==", + "version": "1.0.30001766", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001766.tgz", + "integrity": "sha512-4C0lfJ0/YPjJQHagaE9x2Elb69CIqEPZeG0anQt9SIvIoOH4a4uaRl73IavyO+0qZh6MDLH//DrXThEYKHkmYA==", "funding": [ { "type": "opencollective", @@ -1910,9 +1948,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.266", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.266.tgz", - "integrity": "sha512-kgWEglXvkEfMH7rxP5OSZZwnaDWT7J9EoZCujhnpLbfi0bbNtRkgdX2E3gt0Uer11c61qCYktB3hwkAS325sJg==", + "version": "1.5.279", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.279.tgz", + "integrity": "sha512-0bblUU5UNdOt5G7XqGiJtpZMONma6WAfq9vsFmtn9x1+joAObr6x1chfqyxFSDCAFwFhCQDrqeAr6MYdpwJ9Hg==", "license": "ISC" }, "node_modules/emoji-regex": { @@ -1931,9 +1969,9 @@ } }, "node_modules/enhanced-resolve": { - "version": "5.18.3", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz", - "integrity": "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==", + "version": "5.18.4", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.4.tgz", + "integrity": "sha512-LgQMM4WXU3QI+SYgEc2liRgznaD5ojbmY3sb8LxyguVkIg5FxdpTkvk72te2R38/TGKxH634oLxXRGY6d7AP+Q==", "license": "MIT", "dependencies": { "graceful-fs": "^4.2.4", @@ -2007,9 +2045,9 @@ } }, "node_modules/esbuild": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", - "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.2.tgz", + "integrity": "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==", "hasInstallScript": true, "license": "MIT", "bin": { @@ -2019,32 +2057,32 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.12", - "@esbuild/android-arm": "0.25.12", - "@esbuild/android-arm64": "0.25.12", - "@esbuild/android-x64": "0.25.12", - "@esbuild/darwin-arm64": "0.25.12", - "@esbuild/darwin-x64": "0.25.12", - "@esbuild/freebsd-arm64": "0.25.12", - "@esbuild/freebsd-x64": "0.25.12", - "@esbuild/linux-arm": "0.25.12", - "@esbuild/linux-arm64": "0.25.12", - "@esbuild/linux-ia32": "0.25.12", - "@esbuild/linux-loong64": "0.25.12", - "@esbuild/linux-mips64el": "0.25.12", - "@esbuild/linux-ppc64": "0.25.12", - "@esbuild/linux-riscv64": "0.25.12", - "@esbuild/linux-s390x": "0.25.12", - "@esbuild/linux-x64": "0.25.12", - "@esbuild/netbsd-arm64": "0.25.12", - "@esbuild/netbsd-x64": "0.25.12", - "@esbuild/openbsd-arm64": "0.25.12", - "@esbuild/openbsd-x64": "0.25.12", - "@esbuild/openharmony-arm64": "0.25.12", - "@esbuild/sunos-x64": "0.25.12", - "@esbuild/win32-arm64": "0.25.12", - "@esbuild/win32-ia32": "0.25.12", - "@esbuild/win32-x64": "0.25.12" + "@esbuild/aix-ppc64": "0.27.2", + "@esbuild/android-arm": "0.27.2", + "@esbuild/android-arm64": "0.27.2", + "@esbuild/android-x64": "0.27.2", + "@esbuild/darwin-arm64": "0.27.2", + "@esbuild/darwin-x64": "0.27.2", + "@esbuild/freebsd-arm64": "0.27.2", + "@esbuild/freebsd-x64": "0.27.2", + "@esbuild/linux-arm": "0.27.2", + "@esbuild/linux-arm64": "0.27.2", + "@esbuild/linux-ia32": "0.27.2", + "@esbuild/linux-loong64": "0.27.2", + "@esbuild/linux-mips64el": "0.27.2", + "@esbuild/linux-ppc64": "0.27.2", + "@esbuild/linux-riscv64": "0.27.2", + "@esbuild/linux-s390x": "0.27.2", + "@esbuild/linux-x64": "0.27.2", + "@esbuild/netbsd-arm64": "0.27.2", + "@esbuild/netbsd-x64": "0.27.2", + "@esbuild/openbsd-arm64": "0.27.2", + "@esbuild/openbsd-x64": "0.27.2", + "@esbuild/openharmony-arm64": "0.27.2", + "@esbuild/sunos-x64": "0.27.2", + "@esbuild/win32-arm64": "0.27.2", + "@esbuild/win32-ia32": "0.27.2", + "@esbuild/win32-x64": "0.27.2" } }, "node_modules/escalade": { @@ -2482,9 +2520,9 @@ "license": "MIT" }, "node_modules/laravel-vite-plugin": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/laravel-vite-plugin/-/laravel-vite-plugin-2.0.1.tgz", - "integrity": "sha512-zQuvzWfUKQu9oNVi1o0RZAJCwhGsdhx4NEOyrVQwJHaWDseGP9tl7XUPLY2T8Cj6+IrZ6lmyxlR1KC8unf3RLA==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/laravel-vite-plugin/-/laravel-vite-plugin-2.1.0.tgz", + "integrity": "sha512-z+ck2BSV6KWtYcoIzk9Y5+p4NEjqM+Y4i8/H+VZRLq0OgNjW2DqyADquwYu5j8qRvaXwzNmfCWl1KrMlV1zpsg==", "license": "MIT", "dependencies": { "picocolors": "^1.0.0", @@ -2670,9 +2708,9 @@ } }, "node_modules/lightningcss-linux-x64-gnu": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.2.tgz", - "integrity": "sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==", + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.31.1.tgz", + "integrity": "sha512-xGlFWRMl+0KvUhgySdIaReQdB4FNudfUTARn7q0hh/V67PVGCs3ADFjw+6++kG1RNd0zdGRlEKa+T13/tQjPMA==", "cpu": [ "x64" ], @@ -2749,6 +2787,26 @@ "url": "https://opencollective.com/parcel" } }, + "node_modules/lightningcss/node_modules/lightningcss-linux-x64-gnu": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.2.tgz", + "integrity": "sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, "node_modules/lines-and-columns": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", @@ -2848,15 +2906,6 @@ "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", "license": "MIT" }, - "node_modules/normalize-range": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", - "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -3088,9 +3137,9 @@ } }, "node_modules/rollup": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.53.3.tgz", - "integrity": "sha512-w8GmOxZfBmKknvdXU1sdM9NHcoQejwF/4mNgj2JuEEdRaHwwF12K7e9eXn1nLZ07ad+du76mkVsyeb2rKGllsA==", + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.57.0.tgz", + "integrity": "sha512-e5lPJi/aui4TO1LpAXIRLySmwXSE8k3b9zoGfd42p67wzxog4WHjiZF3M2uheQih4DGyc25QEV4yRBbpueNiUA==", "license": "MIT", "dependencies": { "@types/estree": "1.0.8" @@ -3103,35 +3152,38 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.53.3", - "@rollup/rollup-android-arm64": "4.53.3", - "@rollup/rollup-darwin-arm64": "4.53.3", - "@rollup/rollup-darwin-x64": "4.53.3", - "@rollup/rollup-freebsd-arm64": "4.53.3", - "@rollup/rollup-freebsd-x64": "4.53.3", - "@rollup/rollup-linux-arm-gnueabihf": "4.53.3", - "@rollup/rollup-linux-arm-musleabihf": "4.53.3", - "@rollup/rollup-linux-arm64-gnu": "4.53.3", - "@rollup/rollup-linux-arm64-musl": "4.53.3", - "@rollup/rollup-linux-loong64-gnu": "4.53.3", - "@rollup/rollup-linux-ppc64-gnu": "4.53.3", - "@rollup/rollup-linux-riscv64-gnu": "4.53.3", - "@rollup/rollup-linux-riscv64-musl": "4.53.3", - "@rollup/rollup-linux-s390x-gnu": "4.53.3", - "@rollup/rollup-linux-x64-gnu": "4.53.3", - "@rollup/rollup-linux-x64-musl": "4.53.3", - "@rollup/rollup-openharmony-arm64": "4.53.3", - "@rollup/rollup-win32-arm64-msvc": "4.53.3", - "@rollup/rollup-win32-ia32-msvc": "4.53.3", - "@rollup/rollup-win32-x64-gnu": "4.53.3", - "@rollup/rollup-win32-x64-msvc": "4.53.3", + "@rollup/rollup-android-arm-eabi": "4.57.0", + "@rollup/rollup-android-arm64": "4.57.0", + "@rollup/rollup-darwin-arm64": "4.57.0", + "@rollup/rollup-darwin-x64": "4.57.0", + "@rollup/rollup-freebsd-arm64": "4.57.0", + "@rollup/rollup-freebsd-x64": "4.57.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.57.0", + "@rollup/rollup-linux-arm-musleabihf": "4.57.0", + "@rollup/rollup-linux-arm64-gnu": "4.57.0", + "@rollup/rollup-linux-arm64-musl": "4.57.0", + "@rollup/rollup-linux-loong64-gnu": "4.57.0", + "@rollup/rollup-linux-loong64-musl": "4.57.0", + "@rollup/rollup-linux-ppc64-gnu": "4.57.0", + "@rollup/rollup-linux-ppc64-musl": "4.57.0", + "@rollup/rollup-linux-riscv64-gnu": "4.57.0", + "@rollup/rollup-linux-riscv64-musl": "4.57.0", + "@rollup/rollup-linux-s390x-gnu": "4.57.0", + "@rollup/rollup-linux-x64-gnu": "4.57.0", + "@rollup/rollup-linux-x64-musl": "4.57.0", + "@rollup/rollup-openbsd-x64": "4.57.0", + "@rollup/rollup-openharmony-arm64": "4.57.0", + "@rollup/rollup-win32-arm64-msvc": "4.57.0", + "@rollup/rollup-win32-ia32-msvc": "4.57.0", + "@rollup/rollup-win32-x64-gnu": "4.57.0", + "@rollup/rollup-win32-x64-msvc": "4.57.0", "fsevents": "~2.3.2" } }, "node_modules/rollup/node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.53.3.tgz", - "integrity": "sha512-3EhFi1FU6YL8HTUJZ51imGJWEX//ajQPfqWLI3BQq4TlvHy4X0MOr5q3D2Zof/ka0d5FNdPwZXm3Yyib/UEd+w==", + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.57.0.tgz", + "integrity": "sha512-OR5p5yG5OKSxHReWmwvM0P+VTPMwoBS45PXTMYaskKQqybkS3Kmugq1W+YbNWArF8/s7jQScgzXUhArzEQ7x0A==", "cpu": [ "x64" ], @@ -3290,9 +3342,9 @@ } }, "node_modules/tailwindcss": { - "version": "4.1.17", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.17.tgz", - "integrity": "sha512-j9Ee2YjuQqYT9bbRTfTZht9W/ytp5H+jJpZKiYdP/bpnXARAuELt9ofP0lPnmHjbga7SNQIxdTAXCmtKVYjN+Q==", + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.18.tgz", + "integrity": "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==", "license": "MIT" }, "node_modules/tapable": { @@ -3387,9 +3439,9 @@ "optional": true }, "node_modules/update-browserslist-db": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.2.tgz", - "integrity": "sha512-E85pfNzMQ9jpKkA7+TJAi4TJN+tBCuWh5rUcS/sv6cFi+1q9LYDwDI5dpUL0u/73EElyQ8d3TEaeW4sPedBqYA==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", "funding": [ { "type": "opencollective", @@ -3417,12 +3469,12 @@ } }, "node_modules/vite": { - "version": "7.2.6", - "resolved": "https://registry.npmjs.org/vite/-/vite-7.2.6.tgz", - "integrity": "sha512-tI2l/nFHC5rLh7+5+o7QjKjSR04ivXDF4jcgV0f/bTQ+OJiITy5S6gaynVsEM+7RqzufMnVbIon6Sr5x1SDYaQ==", + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", + "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", "license": "MIT", "dependencies": { - "esbuild": "^0.25.0", + "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", @@ -3548,9 +3600,9 @@ "license": "ISC" }, "node_modules/ws": { - "version": "8.18.3", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", - "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", + "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", "license": "MIT", "engines": { "node": ">=10.0.0" From 3a5becc9510602c3263d35031633e3295619f53d Mon Sep 17 00:00:00 2001 From: Benjamin Nussbaum Date: Wed, 28 Jan 2026 20:27:09 +0100 Subject: [PATCH 071/104] chore: add file to .gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index dd96cc6..390761f 100644 --- a/.gitignore +++ b/.gitignore @@ -39,3 +39,4 @@ yarn-error.log /opencode.json /.cursor /.opencode +/build.sh From 82d53ae304f94e41dd47af2f50c390624296e642 Mon Sep 17 00:00:00 2001 From: Benjamin Nussbaum Date: Thu, 29 Jan 2026 07:58:47 +0100 Subject: [PATCH 072/104] fix: recipes --- resources/views/recipes/holidays-ical.blade.php | 2 +- resources/views/recipes/pollen-forecast-eu.liquid | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/resources/views/recipes/holidays-ical.blade.php b/resources/views/recipes/holidays-ical.blade.php index 454709d..df18a20 100644 --- a/resources/views/recipes/holidays-ical.blade.php +++ b/resources/views/recipes/holidays-ical.blade.php @@ -79,7 +79,7 @@ {{ $event['summary'] }} - {{ $event['location'] ?? '—' }} + {{ Str::limit($event['location'] ?? '—',100) }} @empty diff --git a/resources/views/recipes/pollen-forecast-eu.liquid b/resources/views/recipes/pollen-forecast-eu.liquid index 1dce28f..4140f81 100644 --- a/resources/views/recipes/pollen-forecast-eu.liquid +++ b/resources/views/recipes/pollen-forecast-eu.liquid @@ -1,5 +1,5 @@ - - + +
From bcfc62c7824b9fbce7ec0ef7191ac4d936819b26 Mon Sep 17 00:00:00 2001 From: Benjamin Nussbaum Date: Sat, 31 Jan 2026 17:10:38 +0100 Subject: [PATCH 073/104] chore: update dependencies --- composer.lock | 114 +++++++++++++++++++++++++------------------------- 1 file changed, 57 insertions(+), 57 deletions(-) diff --git a/composer.lock b/composer.lock index df71e7b..c88df33 100644 --- a/composer.lock +++ b/composer.lock @@ -62,16 +62,16 @@ }, { "name": "aws/aws-sdk-php", - "version": "3.369.21", + "version": "3.369.24", "source": { "type": "git", "url": "https://github.com/aws/aws-sdk-php.git", - "reference": "7076af00534135cbbf6cc19eb2521124a3549f0d" + "reference": "17f404a47879c1fb47175ac2b61881ab0dc2dc5c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/7076af00534135cbbf6cc19eb2521124a3549f0d", - "reference": "7076af00534135cbbf6cc19eb2521124a3549f0d", + "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/17f404a47879c1fb47175ac2b61881ab0dc2dc5c", + "reference": "17f404a47879c1fb47175ac2b61881ab0dc2dc5c", "shasum": "" }, "require": { @@ -153,9 +153,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.369.21" + "source": "https://github.com/aws/aws-sdk-php/tree/3.369.24" }, - "time": "2026-01-27T19:14:48+00:00" + "time": "2026-01-30T19:14:32+00:00" }, { "name": "bacon/bacon-qr-code", @@ -214,16 +214,16 @@ }, { "name": "bnussbau/laravel-trmnl-blade", - "version": "2.1.0", + "version": "2.1.1", "source": { "type": "git", "url": "https://github.com/bnussbau/laravel-trmnl-blade.git", - "reference": "1e1cabfead00118d7a80c86ac6108aece2989bc7" + "reference": "6ad96eba917ebc30ebe550e6fce4a995e94f6b35" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/bnussbau/laravel-trmnl-blade/zipball/1e1cabfead00118d7a80c86ac6108aece2989bc7", - "reference": "1e1cabfead00118d7a80c86ac6108aece2989bc7", + "url": "https://api.github.com/repos/bnussbau/laravel-trmnl-blade/zipball/6ad96eba917ebc30ebe550e6fce4a995e94f6b35", + "reference": "6ad96eba917ebc30ebe550e6fce4a995e94f6b35", "shasum": "" }, "require": { @@ -278,7 +278,7 @@ ], "support": { "issues": "https://github.com/bnussbau/laravel-trmnl-blade/issues", - "source": "https://github.com/bnussbau/laravel-trmnl-blade/tree/2.1.0" + "source": "https://github.com/bnussbau/laravel-trmnl-blade/tree/2.1.1" }, "funding": [ { @@ -286,7 +286,7 @@ "type": "buy_me_a_coffee" }, { - "url": "https://usetrmnl.com/?ref=laravel-trmnl", + "url": "https://trmnl.com/?ref=laravel-trmnl", "type": "custom" }, { @@ -294,7 +294,7 @@ "type": "github" } ], - "time": "2026-01-02T20:38:51+00:00" + "time": "2026-01-29T20:40:42+00:00" }, { "name": "bnussbau/trmnl-pipeline-php", @@ -369,16 +369,16 @@ }, { "name": "brick/math", - "version": "0.14.1", + "version": "0.14.2", "source": { "type": "git", "url": "https://github.com/brick/math.git", - "reference": "f05858549e5f9d7bb45875a75583240a38a281d0" + "reference": "55c950aa71a2cabc1d8f2bec1f8a7020bd244aa2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/brick/math/zipball/f05858549e5f9d7bb45875a75583240a38a281d0", - "reference": "f05858549e5f9d7bb45875a75583240a38a281d0", + "url": "https://api.github.com/repos/brick/math/zipball/55c950aa71a2cabc1d8f2bec1f8a7020bd244aa2", + "reference": "55c950aa71a2cabc1d8f2bec1f8a7020bd244aa2", "shasum": "" }, "require": { @@ -417,7 +417,7 @@ ], "support": { "issues": "https://github.com/brick/math/issues", - "source": "https://github.com/brick/math/tree/0.14.1" + "source": "https://github.com/brick/math/tree/0.14.2" }, "funding": [ { @@ -425,7 +425,7 @@ "type": "github" } ], - "time": "2025-11-24T14:40:29+00:00" + "time": "2026-01-30T14:03:11+00:00" }, { "name": "carbonphp/carbon-doctrine-types", @@ -3517,16 +3517,16 @@ }, { "name": "nesbot/carbon", - "version": "3.11.0", + "version": "3.11.1", "source": { "type": "git", "url": "https://github.com/CarbonPHP/carbon.git", - "reference": "bdb375400dcd162624531666db4799b36b64e4a1" + "reference": "f438fcc98f92babee98381d399c65336f3a3827f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/CarbonPHP/carbon/zipball/bdb375400dcd162624531666db4799b36b64e4a1", - "reference": "bdb375400dcd162624531666db4799b36b64e4a1", + "url": "https://api.github.com/repos/CarbonPHP/carbon/zipball/f438fcc98f92babee98381d399c65336f3a3827f", + "reference": "f438fcc98f92babee98381d399c65336f3a3827f", "shasum": "" }, "require": { @@ -3550,7 +3550,7 @@ "phpstan/extension-installer": "^1.4.3", "phpstan/phpstan": "^2.1.22", "phpunit/phpunit": "^10.5.53", - "squizlabs/php_codesniffer": "^3.13.4" + "squizlabs/php_codesniffer": "^3.13.4 || ^4.0.0" }, "bin": [ "bin/carbon" @@ -3593,14 +3593,14 @@ } ], "description": "An API extension for DateTime that supports 281 different languages.", - "homepage": "https://carbon.nesbot.com", + "homepage": "https://carbonphp.github.io/carbon/", "keywords": [ "date", "datetime", "time" ], "support": { - "docs": "https://carbon.nesbot.com/docs", + "docs": "https://carbonphp.github.io/carbon/guide/getting-started/introduction.html", "issues": "https://github.com/CarbonPHP/carbon/issues", "source": "https://github.com/CarbonPHP/carbon" }, @@ -3618,7 +3618,7 @@ "type": "tidelift" } ], - "time": "2025-12-02T21:04:28+00:00" + "time": "2026-01-29T09:26:29+00:00" }, { "name": "nette/schema", @@ -4898,16 +4898,16 @@ }, { "name": "psy/psysh", - "version": "v0.12.18", + "version": "v0.12.19", "source": { "type": "git", "url": "https://github.com/bobthecow/psysh.git", - "reference": "ddff0ac01beddc251786fe70367cd8bbdb258196" + "reference": "a4f766e5c5b6773d8399711019bb7d90875a50ee" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/bobthecow/psysh/zipball/ddff0ac01beddc251786fe70367cd8bbdb258196", - "reference": "ddff0ac01beddc251786fe70367cd8bbdb258196", + "url": "https://api.github.com/repos/bobthecow/psysh/zipball/a4f766e5c5b6773d8399711019bb7d90875a50ee", + "reference": "a4f766e5c5b6773d8399711019bb7d90875a50ee", "shasum": "" }, "require": { @@ -4971,9 +4971,9 @@ ], "support": { "issues": "https://github.com/bobthecow/psysh/issues", - "source": "https://github.com/bobthecow/psysh/tree/v0.12.18" + "source": "https://github.com/bobthecow/psysh/tree/v0.12.19" }, - "time": "2025-12-17T14:35:46+00:00" + "time": "2026-01-30T17:33:13+00:00" }, { "name": "ralouphie/getallheaders", @@ -9066,16 +9066,16 @@ }, { "name": "laravel/boost", - "version": "v2.0.3", + "version": "v2.0.4", "source": { "type": "git", "url": "https://github.com/laravel/boost.git", - "reference": "e9cd3e8219c49459b2e3f2783292e3db42e9b4d2" + "reference": "6f7a9f70c1b2cc5fcef1585e8aa04b8546f150e9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/boost/zipball/e9cd3e8219c49459b2e3f2783292e3db42e9b4d2", - "reference": "e9cd3e8219c49459b2e3f2783292e3db42e9b4d2", + "url": "https://api.github.com/repos/laravel/boost/zipball/6f7a9f70c1b2cc5fcef1585e8aa04b8546f150e9", + "reference": "6f7a9f70c1b2cc5fcef1585e8aa04b8546f150e9", "shasum": "" }, "require": { @@ -9128,7 +9128,7 @@ "issues": "https://github.com/laravel/boost/issues", "source": "https://github.com/laravel/boost" }, - "time": "2026-01-28T08:41:55+00:00" + "time": "2026-01-28T13:53:50+00:00" }, { "name": "laravel/mcp", @@ -10430,11 +10430,11 @@ }, { "name": "phpstan/phpstan", - "version": "2.1.37", + "version": "2.1.38", "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/28cd424c5ea984128c95cfa7ea658808e8954e49", - "reference": "28cd424c5ea984128c95cfa7ea658808e8954e49", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/dfaf1f530e1663aa167bc3e52197adb221582629", + "reference": "dfaf1f530e1663aa167bc3e52197adb221582629", "shasum": "" }, "require": { @@ -10479,7 +10479,7 @@ "type": "github" } ], - "time": "2026-01-24T08:21:55+00:00" + "time": "2026-01-30T17:12:46+00:00" }, { "name": "phpunit/php-code-coverage", @@ -10922,16 +10922,16 @@ }, { "name": "rector/rector", - "version": "2.3.4", + "version": "2.3.5", "source": { "type": "git", "url": "https://github.com/rectorphp/rector.git", - "reference": "9227d7a24b0f23ae941057509364f948d5da9ab2" + "reference": "9442f4037de6a5347ae157fe8e6c7cda9d909070" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/rectorphp/rector/zipball/9227d7a24b0f23ae941057509364f948d5da9ab2", - "reference": "9227d7a24b0f23ae941057509364f948d5da9ab2", + "url": "https://api.github.com/repos/rectorphp/rector/zipball/9442f4037de6a5347ae157fe8e6c7cda9d909070", + "reference": "9442f4037de6a5347ae157fe8e6c7cda9d909070", "shasum": "" }, "require": { @@ -10970,7 +10970,7 @@ ], "support": { "issues": "https://github.com/rectorphp/rector/issues", - "source": "https://github.com/rectorphp/rector/tree/2.3.4" + "source": "https://github.com/rectorphp/rector/tree/2.3.5" }, "funding": [ { @@ -10978,7 +10978,7 @@ "type": "github" } ], - "time": "2026-01-21T14:49:03+00:00" + "time": "2026-01-28T15:22:48+00:00" }, { "name": "sebastian/cli-parser", @@ -11931,24 +11931,24 @@ }, { "name": "ta-tikoma/phpunit-architecture-test", - "version": "0.8.5", + "version": "0.8.6", "source": { "type": "git", "url": "https://github.com/ta-tikoma/phpunit-architecture-test.git", - "reference": "cf6fb197b676ba716837c886baca842e4db29005" + "reference": "ad48430b92901fd7d003fdaf2d7b139f96c0906e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/ta-tikoma/phpunit-architecture-test/zipball/cf6fb197b676ba716837c886baca842e4db29005", - "reference": "cf6fb197b676ba716837c886baca842e4db29005", + "url": "https://api.github.com/repos/ta-tikoma/phpunit-architecture-test/zipball/ad48430b92901fd7d003fdaf2d7b139f96c0906e", + "reference": "ad48430b92901fd7d003fdaf2d7b139f96c0906e", "shasum": "" }, "require": { "nikic/php-parser": "^4.18.0 || ^5.0.0", "php": "^8.1.0", - "phpdocumentor/reflection-docblock": "^5.3.0", - "phpunit/phpunit": "^10.5.5 || ^11.0.0 || ^12.0.0", - "symfony/finder": "^6.4.0 || ^7.0.0" + "phpdocumentor/reflection-docblock": "^5.3.0 || ^6.0.0", + "phpunit/phpunit": "^10.5.5 || ^11.0.0 || ^12.0.0", + "symfony/finder": "^6.4.0 || ^7.0.0 || ^8.0.0" }, "require-dev": { "laravel/pint": "^1.13.7", @@ -11984,9 +11984,9 @@ ], "support": { "issues": "https://github.com/ta-tikoma/phpunit-architecture-test/issues", - "source": "https://github.com/ta-tikoma/phpunit-architecture-test/tree/0.8.5" + "source": "https://github.com/ta-tikoma/phpunit-architecture-test/tree/0.8.6" }, - "time": "2025-04-20T20:23:40+00:00" + "time": "2026-01-30T07:16:00+00:00" }, { "name": "theseer/tokenizer", From 95782512384b64c7487bb55ce22c722ca4950b04 Mon Sep 17 00:00:00 2001 From: Jamie Shiell Date: Sun, 1 Feb 2026 12:30:30 +0000 Subject: [PATCH 074/104] Add maximum_compatibility boolean to devices to address redraw issues with certain hardware (#178) --- app/Models/Device.php | 1 + ...maximum_compatibility_to_devices_table.php | 22 +++++++++++++++++++ .../livewire/devices/configure.blade.php | 9 +++++++- routes/api.php | 1 + tests/Feature/Api/DeviceEndpointsTest.php | 22 +++++++++++++++++++ 5 files changed, 54 insertions(+), 1 deletion(-) create mode 100644 database/migrations/2026_02_01_121714_add_maximum_compatibility_to_devices_table.php diff --git a/app/Models/Device.php b/app/Models/Device.php index 3583f48..a5b0fdf 100644 --- a/app/Models/Device.php +++ b/app/Models/Device.php @@ -42,6 +42,7 @@ class Device extends Model 'sleep_mode_to' => 'datetime:H:i', 'special_function' => 'string', 'pause_until' => 'datetime', + 'maximum_compatibility' => 'boolean', ]; public function getBatteryPercentAttribute(): int|float diff --git a/database/migrations/2026_02_01_121714_add_maximum_compatibility_to_devices_table.php b/database/migrations/2026_02_01_121714_add_maximum_compatibility_to_devices_table.php new file mode 100644 index 0000000..a682c62 --- /dev/null +++ b/database/migrations/2026_02_01_121714_add_maximum_compatibility_to_devices_table.php @@ -0,0 +1,22 @@ +boolean("maximum_compatibility")->default(false); + }); + } + + public function down(): void + { + Schema::table("devices", function (Blueprint $table): void { + $table->dropColumn("maximum_compatibility"); + }); + } +}; diff --git a/resources/views/livewire/devices/configure.blade.php b/resources/views/livewire/devices/configure.blade.php index ce3e821..91095e1 100644 --- a/resources/views/livewire/devices/configure.blade.php +++ b/resources/views/livewire/devices/configure.blade.php @@ -31,6 +31,9 @@ new class extends Component public $device_model_id; + // Signal to device to use high compatibility approaches when redrawing content + public $maximum_compatibility = false; + // Sleep mode and special function public $sleep_mode_enabled = false; @@ -81,6 +84,7 @@ new class extends Component $this->rotate = $device->rotate; $this->image_format = $device->image_format; $this->device_model_id = $device->device_model_id; + $this->maximum_compatibility = $device->maximum_compatibility; $this->deviceModels = DeviceModel::orderBy('label')->get()->sortBy(function ($deviceModel) { // Put TRMNL models at the top, then sort alphabetically within each group $isTrmnl = str_starts_with($deviceModel->label, 'TRMNL'); @@ -141,6 +145,7 @@ new class extends Component 'rotate' => 'required|integer|min:0|max:359', 'image_format' => 'required|string', 'device_model_id' => 'nullable|exists:device_models,id', + 'maximum_compatibility' => 'boolean', 'sleep_mode_enabled' => 'boolean', 'sleep_mode_from' => 'nullable|date_format:H:i', 'sleep_mode_to' => 'nullable|date_format:H:i', @@ -160,6 +165,7 @@ new class extends Component 'rotate' => $this->rotate, 'image_format' => $this->image_format, 'device_model_id' => $deviceModelId, + 'maximum_compatibility' => $this->maximum_compatibility, 'sleep_mode_enabled' => $this->sleep_mode_enabled, 'sleep_mode_from' => $this->sleep_mode_from, 'sleep_mode_to' => $this->sleep_mode_to, @@ -427,6 +433,8 @@ new class extends Component @endforeach + + @if(empty($device_model_id))
@@ -787,4 +795,3 @@ new class extends Component
- diff --git a/routes/api.php b/routes/api.php index d201312..73b2749 100644 --- a/routes/api.php +++ b/routes/api.php @@ -194,6 +194,7 @@ Route::get('/display', function (Request $request) { 'update_firmware' => $device->update_firmware, 'firmware_url' => $device->firmware_url, 'special_function' => $device->special_function ?? 'sleep', + 'maximum_compatibility' => $device->maximum_compatibility, ]; if (config('services.trmnl.image_url_timeout')) { diff --git a/tests/Feature/Api/DeviceEndpointsTest.php b/tests/Feature/Api/DeviceEndpointsTest.php index c98cb2f..54edb10 100644 --- a/tests/Feature/Api/DeviceEndpointsTest.php +++ b/tests/Feature/Api/DeviceEndpointsTest.php @@ -45,6 +45,7 @@ test('device can fetch display data with valid credentials', function (): void { 'update_firmware' => false, 'firmware_url' => null, 'special_function' => 'sleep', + 'maximum_compatibility' => false, ]); expect($device->fresh()) @@ -95,6 +96,27 @@ test('display endpoint omits image_url_timeout when not configured', function () ->assertJsonMissing(['image_url_timeout']); }); +test('display endpoint includes maximum_compatibility value when true for device', function (): void { + $device = Device::factory()->create([ + 'mac_address' => '00:11:22:33:44:55', + 'api_key' => 'test-api-key', + 'maximum_compatibility' => true + ]); + + $response = $this->withHeaders([ + 'id' => $device->mac_address, + 'access-token' => $device->api_key, + 'rssi' => -70, + 'battery_voltage' => 3.8, + 'fw-version' => '1.0.0', + ])->get('/api/display'); + + $response->assertOk() + ->assertJson([ + 'maximum_compatibility' => true, + ]); +}); + test('new device is auto-assigned to user with auto-assign enabled', function (): void { $user = User::factory()->create(['assign_new_devices' => true]); From 1afd8935aff63567aa1bb37bcab94ec203f85dc5 Mon Sep 17 00:00:00 2001 From: Benjamin Nussbaum Date: Sun, 1 Feb 2026 14:26:56 +0100 Subject: [PATCH 075/104] Revise download and star statistics in README Updated download and star counts for TRMNL BYOS Laravel. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 3d6d95e..2231b24 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ [![tests](https://github.com/usetrmnl/byos_laravel/actions/workflows/test.yml/badge.svg)](https://github.com/usetrmnl/byos_laravel/actions/workflows/test.yml) TRMNL BYOS Laravel is a self-hostable implementation of a TRMNL server, built with Laravel. -It allows you to manage TRMNL devices, generate screens using **native plugins** (Screens API, Markup), **recipes** (120+ from the [OSS community catalog](https://bnussbau.github.io/trmnl-recipe-catalog/), 600+ from the [TRMNL catalog](https://trmnl.com/recipes), or your own), or the **API**, and can also act as a **proxy** for the native cloud service (Core). With over 40k downloads and 160+ stars, it’s the most popular community-driven BYOS. +It allows you to manage TRMNL devices, generate screens using **native plugins** (Screens API, Markup), **recipes** (130+ from the [OSS community catalog](https://bnussbau.github.io/trmnl-recipe-catalog/), 700+ from the [TRMNL catalog](https://trmnl.com/recipes), or your own), or the **API**, and can also act as a **proxy** for the native cloud service (Core). With over 50k downloads and 200+ stars, it’s the most popular community-driven BYOS. ![Screenshot](README_byos-screenshot.png) ![Screenshot](README_byos-screenshot-dark.png) From bb0f2a4555c17759a5692ebe1ec0f6ccc1542bc3 Mon Sep 17 00:00:00 2001 From: Benjamin Nussbaum Date: Tue, 3 Feb 2026 21:40:26 +0100 Subject: [PATCH 076/104] chore: update dependencies --- composer.lock | 110 ++++++++++++++++++++++++++++---------------------- 1 file changed, 61 insertions(+), 49 deletions(-) diff --git a/composer.lock b/composer.lock index c88df33..0719a49 100644 --- a/composer.lock +++ b/composer.lock @@ -62,16 +62,16 @@ }, { "name": "aws/aws-sdk-php", - "version": "3.369.24", + "version": "3.369.26", "source": { "type": "git", "url": "https://github.com/aws/aws-sdk-php.git", - "reference": "17f404a47879c1fb47175ac2b61881ab0dc2dc5c" + "reference": "ad0916c6595d98f9052f60e1d7204f4740369e94" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/17f404a47879c1fb47175ac2b61881ab0dc2dc5c", - "reference": "17f404a47879c1fb47175ac2b61881ab0dc2dc5c", + "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/ad0916c6595d98f9052f60e1d7204f4740369e94", + "reference": "ad0916c6595d98f9052f60e1d7204f4740369e94", "shasum": "" }, "require": { @@ -153,9 +153,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.369.24" + "source": "https://github.com/aws/aws-sdk-php/tree/3.369.26" }, - "time": "2026-01-30T19:14:32+00:00" + "time": "2026-02-03T19:16:42+00:00" }, { "name": "bacon/bacon-qr-code", @@ -369,16 +369,16 @@ }, { "name": "brick/math", - "version": "0.14.2", + "version": "0.14.5", "source": { "type": "git", "url": "https://github.com/brick/math.git", - "reference": "55c950aa71a2cabc1d8f2bec1f8a7020bd244aa2" + "reference": "618a8077b3c326045e10d5788ed713b341fcfe40" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/brick/math/zipball/55c950aa71a2cabc1d8f2bec1f8a7020bd244aa2", - "reference": "55c950aa71a2cabc1d8f2bec1f8a7020bd244aa2", + "url": "https://api.github.com/repos/brick/math/zipball/618a8077b3c326045e10d5788ed713b341fcfe40", + "reference": "618a8077b3c326045e10d5788ed713b341fcfe40", "shasum": "" }, "require": { @@ -417,7 +417,7 @@ ], "support": { "issues": "https://github.com/brick/math/issues", - "source": "https://github.com/brick/math/tree/0.14.2" + "source": "https://github.com/brick/math/tree/0.14.5" }, "funding": [ { @@ -425,7 +425,7 @@ "type": "github" } ], - "time": "2026-01-30T14:03:11+00:00" + "time": "2026-02-03T18:06:51+00:00" }, { "name": "carbonphp/carbon-doctrine-types", @@ -3194,16 +3194,16 @@ }, { "name": "livewire/livewire", - "version": "v4.1.0", + "version": "v4.1.2", "source": { "type": "git", "url": "https://github.com/livewire/livewire.git", - "reference": "4ae4ee18448f8e9d97b68c8c091b2b597f852a6f" + "reference": "8adef21f35f4ffa87fd2f3655b350236df0c39a8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/livewire/livewire/zipball/4ae4ee18448f8e9d97b68c8c091b2b597f852a6f", - "reference": "4ae4ee18448f8e9d97b68c8c091b2b597f852a6f", + "url": "https://api.github.com/repos/livewire/livewire/zipball/8adef21f35f4ffa87fd2f3655b350236df0c39a8", + "reference": "8adef21f35f4ffa87fd2f3655b350236df0c39a8", "shasum": "" }, "require": { @@ -3258,7 +3258,7 @@ "description": "A front-end framework for Laravel.", "support": { "issues": "https://github.com/livewire/livewire/issues", - "source": "https://github.com/livewire/livewire/tree/v4.1.0" + "source": "https://github.com/livewire/livewire/tree/v4.1.2" }, "funding": [ { @@ -3266,7 +3266,7 @@ "type": "github" } ], - "time": "2026-01-27T02:21:37+00:00" + "time": "2026-02-03T03:01:29+00:00" }, { "name": "maennchen/zipstream-php", @@ -3687,16 +3687,16 @@ }, { "name": "nette/utils", - "version": "v4.1.1", + "version": "v4.1.2", "source": { "type": "git", "url": "https://github.com/nette/utils.git", - "reference": "c99059c0315591f1a0db7ad6002000288ab8dc72" + "reference": "f76b5dc3d6c6d3043c8d937df2698515b99cbaf5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nette/utils/zipball/c99059c0315591f1a0db7ad6002000288ab8dc72", - "reference": "c99059c0315591f1a0db7ad6002000288ab8dc72", + "url": "https://api.github.com/repos/nette/utils/zipball/f76b5dc3d6c6d3043c8d937df2698515b99cbaf5", + "reference": "f76b5dc3d6c6d3043c8d937df2698515b99cbaf5", "shasum": "" }, "require": { @@ -3709,7 +3709,7 @@ "require-dev": { "jetbrains/phpstorm-attributes": "^1.2", "nette/tester": "^2.5", - "phpstan/phpstan-nette": "^2.0@stable", + "phpstan/phpstan": "^2.0@stable", "tracy/tracy": "^2.9" }, "suggest": { @@ -3770,9 +3770,9 @@ ], "support": { "issues": "https://github.com/nette/utils/issues", - "source": "https://github.com/nette/utils/tree/v4.1.1" + "source": "https://github.com/nette/utils/tree/v4.1.2" }, - "time": "2025-12-22T12:14:32+00:00" + "time": "2026-02-03T17:21:09+00:00" }, { "name": "nikic/php-parser", @@ -8875,16 +8875,16 @@ }, { "name": "iamcal/sql-parser", - "version": "v0.6", + "version": "v0.7", "source": { "type": "git", "url": "https://github.com/iamcal/SQLParser.git", - "reference": "947083e2dca211a6f12fb1beb67a01e387de9b62" + "reference": "610392f38de49a44dab08dc1659960a29874c4b8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/iamcal/SQLParser/zipball/947083e2dca211a6f12fb1beb67a01e387de9b62", - "reference": "947083e2dca211a6f12fb1beb67a01e387de9b62", + "url": "https://api.github.com/repos/iamcal/SQLParser/zipball/610392f38de49a44dab08dc1659960a29874c4b8", + "reference": "610392f38de49a44dab08dc1659960a29874c4b8", "shasum": "" }, "require-dev": { @@ -8910,9 +8910,9 @@ "description": "MySQL schema parser", "support": { "issues": "https://github.com/iamcal/SQLParser/issues", - "source": "https://github.com/iamcal/SQLParser/tree/v0.6" + "source": "https://github.com/iamcal/SQLParser/tree/v0.7" }, - "time": "2025-03-17T16:59:46+00:00" + "time": "2026-01-28T22:20:33+00:00" }, { "name": "jean85/pretty-package-versions", @@ -8976,21 +8976,21 @@ }, { "name": "larastan/larastan", - "version": "v3.9.1", + "version": "v3.9.2", "source": { "type": "git", "url": "https://github.com/larastan/larastan.git", - "reference": "4b92d9627f779fd32bdc16f53f8ce88c50446ff5" + "reference": "2e9ed291bdc1969e7f270fb33c9cdf3c912daeb2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/larastan/larastan/zipball/4b92d9627f779fd32bdc16f53f8ce88c50446ff5", - "reference": "4b92d9627f779fd32bdc16f53f8ce88c50446ff5", + "url": "https://api.github.com/repos/larastan/larastan/zipball/2e9ed291bdc1969e7f270fb33c9cdf3c912daeb2", + "reference": "2e9ed291bdc1969e7f270fb33c9cdf3c912daeb2", "shasum": "" }, "require": { "ext-json": "*", - "iamcal/sql-parser": "^0.6.0", + "iamcal/sql-parser": "^0.7.0", "illuminate/console": "^11.44.2 || ^12.4.1", "illuminate/container": "^11.44.2 || ^12.4.1", "illuminate/contracts": "^11.44.2 || ^12.4.1", @@ -9054,7 +9054,7 @@ ], "support": { "issues": "https://github.com/larastan/larastan/issues", - "source": "https://github.com/larastan/larastan/tree/v3.9.1" + "source": "https://github.com/larastan/larastan/tree/v3.9.2" }, "funding": [ { @@ -9062,20 +9062,20 @@ "type": "github" } ], - "time": "2026-01-21T09:15:17+00:00" + "time": "2026-01-30T15:16:32+00:00" }, { "name": "laravel/boost", - "version": "v2.0.4", + "version": "v2.0.5", "source": { "type": "git", "url": "https://github.com/laravel/boost.git", - "reference": "6f7a9f70c1b2cc5fcef1585e8aa04b8546f150e9" + "reference": "00eede2041a9bac83eabbd3b3f16bd4aa91277c9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/boost/zipball/6f7a9f70c1b2cc5fcef1585e8aa04b8546f150e9", - "reference": "6f7a9f70c1b2cc5fcef1585e8aa04b8546f150e9", + "url": "https://api.github.com/repos/laravel/boost/zipball/00eede2041a9bac83eabbd3b3f16bd4aa91277c9", + "reference": "00eede2041a9bac83eabbd3b3f16bd4aa91277c9", "shasum": "" }, "require": { @@ -9128,7 +9128,7 @@ "issues": "https://github.com/laravel/boost/issues", "source": "https://github.com/laravel/boost" }, - "time": "2026-01-28T13:53:50+00:00" + "time": "2026-02-01T09:52:44+00:00" }, { "name": "laravel/mcp", @@ -10572,16 +10572,16 @@ }, { "name": "phpunit/php-file-iterator", - "version": "6.0.0", + "version": "6.0.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-file-iterator.git", - "reference": "961bc913d42fe24a257bfff826a5068079ac7782" + "reference": "3d1cd096ef6bea4bf2762ba586e35dbd317cbfd5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/961bc913d42fe24a257bfff826a5068079ac7782", - "reference": "961bc913d42fe24a257bfff826a5068079ac7782", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/3d1cd096ef6bea4bf2762ba586e35dbd317cbfd5", + "reference": "3d1cd096ef6bea4bf2762ba586e35dbd317cbfd5", "shasum": "" }, "require": { @@ -10621,15 +10621,27 @@ "support": { "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues", "security": "https://github.com/sebastianbergmann/php-file-iterator/security/policy", - "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/6.0.0" + "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/6.0.1" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/php-file-iterator", + "type": "tidelift" } ], - "time": "2025-02-07T04:58:37+00:00" + "time": "2026-02-02T14:04:18+00:00" }, { "name": "phpunit/php-invoker", From 7adbcc104e409a6c71ce2f34a91917a60af0ca5f Mon Sep 17 00:00:00 2001 From: Benjamin Nussbaum Date: Tue, 3 Feb 2026 22:04:09 +0100 Subject: [PATCH 077/104] chore: update maximum compatibility wording --- resources/views/livewire/devices/configure.blade.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/views/livewire/devices/configure.blade.php b/resources/views/livewire/devices/configure.blade.php index 91095e1..93183cf 100644 --- a/resources/views/livewire/devices/configure.blade.php +++ b/resources/views/livewire/devices/configure.blade.php @@ -433,7 +433,7 @@ new class extends Component @endforeach - + @if(empty($device_model_id)) From 2b919a193d5602f4d50c9826dc734801cf9710dd Mon Sep 17 00:00:00 2001 From: Benjamin Nussbaum Date: Tue, 3 Feb 2026 22:21:35 +0100 Subject: [PATCH 078/104] fix(#176): inject device variables into recipes --- app/Models/PlaylistItem.php | 4 +-- app/Models/Plugin.php | 7 +++++ tests/Unit/Models/PlaylistItemTest.php | 38 ++++++++++++++++++++++++++ 3 files changed, 47 insertions(+), 2 deletions(-) diff --git a/app/Models/PlaylistItem.php b/app/Models/PlaylistItem.php index ad11f1d..31a6b69 100644 --- a/app/Models/PlaylistItem.php +++ b/app/Models/PlaylistItem.php @@ -143,7 +143,7 @@ class PlaylistItem extends Model 'deviceVariant' => $device?->deviceVariant() ?? 'og', 'scaleLevel' => $device?->scaleLevel(), 'slot' => $this->plugin instanceof Plugin - ? $this->plugin->render('full', false) + ? $this->plugin->render('full', false, $device) : throw new Exception('Invalid plugin instance'), ])->render(); } @@ -157,7 +157,7 @@ class PlaylistItem extends Model foreach ($plugins as $index => $plugin) { $size = $this->getLayoutSize($index); - $pluginMarkups[] = $plugin->render($size, false); + $pluginMarkups[] = $plugin->render($size, false, $device); } return view('trmnl-layouts.mashup', [ diff --git a/app/Models/Plugin.php b/app/Models/Plugin.php index dceb795..bc46559 100644 --- a/app/Models/Plugin.php +++ b/app/Models/Plugin.php @@ -447,6 +447,13 @@ class Plugin extends Model 'locale' => 'en', 'time_zone_iana' => $timezone, ], + 'device' => [ + 'friendly_id' => $device?->friendly_id, + 'percent_charged' => $device?->battery_percent, + 'wifi_strength' => $device?->wifi_strength, + 'height' => $device?->height, + 'width' => $device?->width, + ], 'plugin_settings' => [ 'instance_name' => $this->name, 'strategy' => $this->data_strategy, diff --git a/tests/Unit/Models/PlaylistItemTest.php b/tests/Unit/Models/PlaylistItemTest.php index 428a165..162ba4c 100644 --- a/tests/Unit/Models/PlaylistItemTest.php +++ b/tests/Unit/Models/PlaylistItemTest.php @@ -1,8 +1,10 @@ create(); @@ -208,3 +210,39 @@ test('playlist item can create mashup', function (): void { ->is_active->toBeTrue() ->order->toBe($order); }); + +test('playlist item mashup render includes device context in liquid (trmnl.device.friendly_id)', function (): void { + $user = User::factory()->create(); + $device = Device::factory()->create([ + 'user_id' => $user->id, + 'friendly_id' => 'my-kitchen-display', + ]); + $playlist = Playlist::factory()->create(['device_id' => $device->id]); + + $plugin1 = Plugin::factory()->create([ + 'user_id' => $user->id, + 'plugin_type' => 'recipe', + 'markup_language' => 'liquid', + 'render_markup' => '{{ trmnl.device.friendly_id }}', + ]); + $plugin2 = Plugin::factory()->create([ + 'user_id' => $user->id, + 'plugin_type' => 'recipe', + 'markup_language' => 'liquid', + 'render_markup' => 'slot2:{{ trmnl.device.friendly_id }}', + ]); + + $playlistItem = PlaylistItem::createMashup( + $playlist, + '1Lx1R', + [$plugin1->id, $plugin2->id], + 'Device context mashup', + 1 + ); + + $markup = $playlistItem->render($device); + + expect($markup) + ->toContain('my-kitchen-display') + ->toContain('slot2:my-kitchen-display'); +}); From 0f61861c5e902379a8865afa68c43d4058dfeeb1 Mon Sep 17 00:00:00 2001 From: Benjamin Nussbaum Date: Thu, 5 Feb 2026 16:38:14 +0100 Subject: [PATCH 079/104] chore: update dependencies --- .gitignore | 1 + composer.lock | 173 +++++++++++++++++++++++++------------------------- 2 files changed, 88 insertions(+), 86 deletions(-) diff --git a/.gitignore b/.gitignore index 390761f..9c0185e 100644 --- a/.gitignore +++ b/.gitignore @@ -40,3 +40,4 @@ yarn-error.log /.cursor /.opencode /build.sh +/.junie diff --git a/composer.lock b/composer.lock index 0719a49..80c106a 100644 --- a/composer.lock +++ b/composer.lock @@ -62,16 +62,16 @@ }, { "name": "aws/aws-sdk-php", - "version": "3.369.26", + "version": "3.369.27", "source": { "type": "git", "url": "https://github.com/aws/aws-sdk-php.git", - "reference": "ad0916c6595d98f9052f60e1d7204f4740369e94" + "reference": "f844afab2a74eb3cf881970a9c31de460510eb74" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/ad0916c6595d98f9052f60e1d7204f4740369e94", - "reference": "ad0916c6595d98f9052f60e1d7204f4740369e94", + "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/f844afab2a74eb3cf881970a9c31de460510eb74", + "reference": "f844afab2a74eb3cf881970a9c31de460510eb74", "shasum": "" }, "require": { @@ -153,9 +153,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.369.26" + "source": "https://github.com/aws/aws-sdk-php/tree/3.369.27" }, - "time": "2026-02-03T19:16:42+00:00" + "time": "2026-02-04T19:07:08+00:00" }, { "name": "bacon/bacon-qr-code", @@ -369,16 +369,16 @@ }, { "name": "brick/math", - "version": "0.14.5", + "version": "0.14.6", "source": { "type": "git", "url": "https://github.com/brick/math.git", - "reference": "618a8077b3c326045e10d5788ed713b341fcfe40" + "reference": "32498d5e1897e7642c0b961ace2df6d7dc9a3bc3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/brick/math/zipball/618a8077b3c326045e10d5788ed713b341fcfe40", - "reference": "618a8077b3c326045e10d5788ed713b341fcfe40", + "url": "https://api.github.com/repos/brick/math/zipball/32498d5e1897e7642c0b961ace2df6d7dc9a3bc3", + "reference": "32498d5e1897e7642c0b961ace2df6d7dc9a3bc3", "shasum": "" }, "require": { @@ -417,7 +417,7 @@ ], "support": { "issues": "https://github.com/brick/math/issues", - "source": "https://github.com/brick/math/tree/0.14.5" + "source": "https://github.com/brick/math/tree/0.14.6" }, "funding": [ { @@ -425,7 +425,7 @@ "type": "github" } ], - "time": "2026-02-03T18:06:51+00:00" + "time": "2026-02-05T07:59:58+00:00" }, { "name": "carbonphp/carbon-doctrine-types", @@ -1831,28 +1831,28 @@ }, { "name": "laravel/fortify", - "version": "v1.34.0", + "version": "v1.34.1", "source": { "type": "git", "url": "https://github.com/laravel/fortify.git", - "reference": "c322715f2786210a722ed56966f7c9877b653b25" + "reference": "412575e9c0cb21d49a30b7045ad4902019f538c2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/fortify/zipball/c322715f2786210a722ed56966f7c9877b653b25", - "reference": "c322715f2786210a722ed56966f7c9877b653b25", + "url": "https://api.github.com/repos/laravel/fortify/zipball/412575e9c0cb21d49a30b7045ad4902019f538c2", + "reference": "412575e9c0cb21d49a30b7045ad4902019f538c2", "shasum": "" }, "require": { "bacon/bacon-qr-code": "^3.0", "ext-json": "*", - "illuminate/support": "^10.0|^11.0|^12.0", + "illuminate/console": "^10.0|^11.0|^12.0|^13.0", + "illuminate/support": "^10.0|^11.0|^12.0|^13.0", "php": "^8.1", - "pragmarx/google2fa": "^9.0", - "symfony/console": "^6.0|^7.0" + "pragmarx/google2fa": "^9.0" }, "require-dev": { - "orchestra/testbench": "^8.36|^9.15|^10.8", + "orchestra/testbench": "^8.36|^9.15|^10.8|^11.0", "phpstan/phpstan": "^1.10" }, "type": "library", @@ -1890,20 +1890,20 @@ "issues": "https://github.com/laravel/fortify/issues", "source": "https://github.com/laravel/fortify" }, - "time": "2026-01-26T10:23:19+00:00" + "time": "2026-02-03T06:55:55+00:00" }, { "name": "laravel/framework", - "version": "v12.49.0", + "version": "v12.50.0", "source": { "type": "git", "url": "https://github.com/laravel/framework.git", - "reference": "4bde4530545111d8bdd1de6f545fa8824039fcb5" + "reference": "174ffed91d794a35a541a5eb7c3785a02a34aaba" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/framework/zipball/4bde4530545111d8bdd1de6f545fa8824039fcb5", - "reference": "4bde4530545111d8bdd1de6f545fa8824039fcb5", + "url": "https://api.github.com/repos/laravel/framework/zipball/174ffed91d794a35a541a5eb7c3785a02a34aaba", + "reference": "174ffed91d794a35a541a5eb7c3785a02a34aaba", "shasum": "" }, "require": { @@ -2112,34 +2112,34 @@ "issues": "https://github.com/laravel/framework/issues", "source": "https://github.com/laravel/framework" }, - "time": "2026-01-28T03:40:49+00:00" + "time": "2026-02-04T18:34:13+00:00" }, { "name": "laravel/prompts", - "version": "v0.3.11", + "version": "v0.3.12", "source": { "type": "git", "url": "https://github.com/laravel/prompts.git", - "reference": "dd2a2ed95acacbcccd32fd98dee4c946ae7a7217" + "reference": "4861ded9003b7f8a158176a0b7666f74ee761be8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/prompts/zipball/dd2a2ed95acacbcccd32fd98dee4c946ae7a7217", - "reference": "dd2a2ed95acacbcccd32fd98dee4c946ae7a7217", + "url": "https://api.github.com/repos/laravel/prompts/zipball/4861ded9003b7f8a158176a0b7666f74ee761be8", + "reference": "4861ded9003b7f8a158176a0b7666f74ee761be8", "shasum": "" }, "require": { "composer-runtime-api": "^2.2", "ext-mbstring": "*", "php": "^8.1", - "symfony/console": "^6.2|^7.0" + "symfony/console": "^6.2|^7.0|^8.0" }, "conflict": { "illuminate/console": ">=10.17.0 <10.25.0", "laravel/framework": ">=10.17.0 <10.25.0" }, "require-dev": { - "illuminate/collections": "^10.0|^11.0|^12.0", + "illuminate/collections": "^10.0|^11.0|^12.0|^13.0", "mockery/mockery": "^1.5", "pestphp/pest": "^2.3|^3.4|^4.0", "phpstan/phpstan": "^1.12.28", @@ -2169,9 +2169,9 @@ "description": "Add beautiful and user-friendly forms to your command-line applications.", "support": { "issues": "https://github.com/laravel/prompts/issues", - "source": "https://github.com/laravel/prompts/tree/v0.3.11" + "source": "https://github.com/laravel/prompts/tree/v0.3.12" }, - "time": "2026-01-27T02:55:06+00:00" + "time": "2026-02-03T06:57:26+00:00" }, { "name": "laravel/sanctum", @@ -2238,27 +2238,27 @@ }, { "name": "laravel/serializable-closure", - "version": "v2.0.8", + "version": "v2.0.9", "source": { "type": "git", "url": "https://github.com/laravel/serializable-closure.git", - "reference": "7581a4407012f5f53365e11bafc520fd7f36bc9b" + "reference": "8f631589ab07b7b52fead814965f5a800459cb3e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/serializable-closure/zipball/7581a4407012f5f53365e11bafc520fd7f36bc9b", - "reference": "7581a4407012f5f53365e11bafc520fd7f36bc9b", + "url": "https://api.github.com/repos/laravel/serializable-closure/zipball/8f631589ab07b7b52fead814965f5a800459cb3e", + "reference": "8f631589ab07b7b52fead814965f5a800459cb3e", "shasum": "" }, "require": { "php": "^8.1" }, "require-dev": { - "illuminate/support": "^10.0|^11.0|^12.0", + "illuminate/support": "^10.0|^11.0|^12.0|^13.0", "nesbot/carbon": "^2.67|^3.0", "pestphp/pest": "^2.36|^3.0|^4.0", "phpstan/phpstan": "^2.0", - "symfony/var-dumper": "^6.2.0|^7.0.0" + "symfony/var-dumper": "^6.2.0|^7.0.0|^8.0.0" }, "type": "library", "extra": { @@ -2295,7 +2295,7 @@ "issues": "https://github.com/laravel/serializable-closure/issues", "source": "https://github.com/laravel/serializable-closure" }, - "time": "2026-01-08T16:22:46+00:00" + "time": "2026-02-03T06:55:34+00:00" }, { "name": "laravel/socialite", @@ -8536,16 +8536,16 @@ "packages-dev": [ { "name": "brianium/paratest", - "version": "v7.16.1", + "version": "v7.17.0", "source": { "type": "git", "url": "https://github.com/paratestphp/paratest.git", - "reference": "f0fdfd8e654e0d38bc2ba756a6cabe7be287390b" + "reference": "53cb90a6aa3ef3840458781600628ade058a18b9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/paratestphp/paratest/zipball/f0fdfd8e654e0d38bc2ba756a6cabe7be287390b", - "reference": "f0fdfd8e654e0d38bc2ba756a6cabe7be287390b", + "url": "https://api.github.com/repos/paratestphp/paratest/zipball/53cb90a6aa3ef3840458781600628ade058a18b9", + "reference": "53cb90a6aa3ef3840458781600628ade058a18b9", "shasum": "" }, "require": { @@ -8559,7 +8559,7 @@ "phpunit/php-code-coverage": "^12.5.2", "phpunit/php-file-iterator": "^6", "phpunit/php-timer": "^8", - "phpunit/phpunit": "^12.5.4", + "phpunit/phpunit": "^12.5.8", "sebastian/environment": "^8.0.3", "symfony/console": "^7.3.4 || ^8.0.0", "symfony/process": "^7.3.4 || ^8.0.0" @@ -8569,10 +8569,10 @@ "ext-pcntl": "*", "ext-pcov": "*", "ext-posix": "*", - "phpstan/phpstan": "^2.1.33", + "phpstan/phpstan": "^2.1.38", "phpstan/phpstan-deprecation-rules": "^2.0.3", - "phpstan/phpstan-phpunit": "^2.0.11", - "phpstan/phpstan-strict-rules": "^2.0.7", + "phpstan/phpstan-phpunit": "^2.0.12", + "phpstan/phpstan-strict-rules": "^2.0.8", "symfony/filesystem": "^7.3.2 || ^8.0.0" }, "bin": [ @@ -8613,7 +8613,7 @@ ], "support": { "issues": "https://github.com/paratestphp/paratest/issues", - "source": "https://github.com/paratestphp/paratest/tree/v7.16.1" + "source": "https://github.com/paratestphp/paratest/tree/v7.17.0" }, "funding": [ { @@ -8625,7 +8625,7 @@ "type": "paypal" } ], - "time": "2026-01-08T07:23:06+00:00" + "time": "2026-02-05T09:14:44+00:00" }, { "name": "fakerphp/faker", @@ -9066,16 +9066,16 @@ }, { "name": "laravel/boost", - "version": "v2.0.5", + "version": "v2.0.6", "source": { "type": "git", "url": "https://github.com/laravel/boost.git", - "reference": "00eede2041a9bac83eabbd3b3f16bd4aa91277c9" + "reference": "1e1cb76e8e87ca3dd3c3d64deccbc97f4de38215" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/boost/zipball/00eede2041a9bac83eabbd3b3f16bd4aa91277c9", - "reference": "00eede2041a9bac83eabbd3b3f16bd4aa91277c9", + "url": "https://api.github.com/repos/laravel/boost/zipball/1e1cb76e8e87ca3dd3c3d64deccbc97f4de38215", + "reference": "1e1cb76e8e87ca3dd3c3d64deccbc97f4de38215", "shasum": "" }, "require": { @@ -9128,39 +9128,39 @@ "issues": "https://github.com/laravel/boost/issues", "source": "https://github.com/laravel/boost" }, - "time": "2026-02-01T09:52:44+00:00" + "time": "2026-02-04T10:10:48+00:00" }, { "name": "laravel/mcp", - "version": "v0.5.3", + "version": "v0.5.5", "source": { "type": "git", "url": "https://github.com/laravel/mcp.git", - "reference": "39b9791b989927642137dd5b55dde0529f1614f9" + "reference": "b3327bb75fd2327577281e507e2dbc51649513d6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/mcp/zipball/39b9791b989927642137dd5b55dde0529f1614f9", - "reference": "39b9791b989927642137dd5b55dde0529f1614f9", + "url": "https://api.github.com/repos/laravel/mcp/zipball/b3327bb75fd2327577281e507e2dbc51649513d6", + "reference": "b3327bb75fd2327577281e507e2dbc51649513d6", "shasum": "" }, "require": { "ext-json": "*", "ext-mbstring": "*", - "illuminate/console": "^10.49.0|^11.45.3|^12.41.1", - "illuminate/container": "^10.49.0|^11.45.3|^12.41.1", - "illuminate/contracts": "^10.49.0|^11.45.3|^12.41.1", - "illuminate/http": "^10.49.0|^11.45.3|^12.41.1", - "illuminate/json-schema": "^12.41.1", - "illuminate/routing": "^10.49.0|^11.45.3|^12.41.1", - "illuminate/support": "^10.49.0|^11.45.3|^12.41.1", - "illuminate/validation": "^10.49.0|^11.45.3|^12.41.1", - "php": "^8.1" + "illuminate/console": "^11.45.3|^12.41.1|^13.0", + "illuminate/container": "^11.45.3|^12.41.1|^13.0", + "illuminate/contracts": "^11.45.3|^12.41.1|^13.0", + "illuminate/http": "^11.45.3|^12.41.1|^13.0", + "illuminate/json-schema": "^12.41.1|^13.0", + "illuminate/routing": "^11.45.3|^12.41.1|^13.0", + "illuminate/support": "^11.45.3|^12.41.1|^13.0", + "illuminate/validation": "^11.45.3|^12.41.1|^13.0", + "php": "^8.2" }, "require-dev": { "laravel/pint": "^1.20", - "orchestra/testbench": "^8.36|^9.15|^10.8", - "pestphp/pest": "^2.36.0|^3.8.4|^4.1.0", + "orchestra/testbench": "^9.15|^10.8|^11.0", + "pestphp/pest": "^3.8.5|^4.3.2", "phpstan/phpstan": "^2.1.27", "rector/rector": "^2.2.4" }, @@ -9201,41 +9201,42 @@ "issues": "https://github.com/laravel/mcp/issues", "source": "https://github.com/laravel/mcp" }, - "time": "2026-01-26T10:25:21+00:00" + "time": "2026-02-05T14:05:18+00:00" }, { "name": "laravel/pail", - "version": "v1.2.4", + "version": "v1.2.5", "source": { "type": "git", "url": "https://github.com/laravel/pail.git", - "reference": "49f92285ff5d6fc09816e976a004f8dec6a0ea30" + "reference": "fdb73f5eacf03db576c710d5a00101ba185f2254" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/pail/zipball/49f92285ff5d6fc09816e976a004f8dec6a0ea30", - "reference": "49f92285ff5d6fc09816e976a004f8dec6a0ea30", + "url": "https://api.github.com/repos/laravel/pail/zipball/fdb73f5eacf03db576c710d5a00101ba185f2254", + "reference": "fdb73f5eacf03db576c710d5a00101ba185f2254", "shasum": "" }, "require": { "ext-mbstring": "*", - "illuminate/console": "^10.24|^11.0|^12.0", - "illuminate/contracts": "^10.24|^11.0|^12.0", - "illuminate/log": "^10.24|^11.0|^12.0", - "illuminate/process": "^10.24|^11.0|^12.0", - "illuminate/support": "^10.24|^11.0|^12.0", + "illuminate/console": "^10.24|^11.0|^12.0|^13.0", + "illuminate/contracts": "^10.24|^11.0|^12.0|^13.0", + "illuminate/log": "^10.24|^11.0|^12.0|^13.0", + "illuminate/process": "^10.24|^11.0|^12.0|^13.0", + "illuminate/support": "^10.24|^11.0|^12.0|^13.0", "nunomaduro/termwind": "^1.15|^2.0", "php": "^8.2", - "symfony/console": "^6.0|^7.0" + "symfony/console": "^6.0|^7.0|^8.0" }, "require-dev": { - "laravel/framework": "^10.24|^11.0|^12.0", + "laravel/framework": "^10.24|^11.0|^12.0|^13.0", "laravel/pint": "^1.13", - "orchestra/testbench-core": "^8.13|^9.17|^10.8", + "orchestra/testbench-core": "^8.13|^9.17|^10.8|^11.0", "pestphp/pest": "^2.20|^3.0|^4.0", "pestphp/pest-plugin-type-coverage": "^2.3|^3.0|^4.0", "phpstan/phpstan": "^1.12.27", - "symfony/var-dumper": "^6.3|^7.0" + "symfony/var-dumper": "^6.3|^7.0|^8.0", + "symfony/yaml": "^6.3|^7.0|^8.0" }, "type": "library", "extra": { @@ -9280,7 +9281,7 @@ "issues": "https://github.com/laravel/pail/issues", "source": "https://github.com/laravel/pail" }, - "time": "2025-11-20T16:29:35+00:00" + "time": "2026-02-04T15:10:32+00:00" }, { "name": "laravel/pint", From 06e684227e123e4b6bb34cfa52b68c689ea7ff33 Mon Sep 17 00:00:00 2001 From: Benjamin Nussbaum Date: Thu, 5 Feb 2026 19:17:51 +0100 Subject: [PATCH 080/104] chore: bump laravel-trmnl-blade to 2.2.1 --- composer.json | 2 +- composer.lock | 14 +++++++------- .../views/vendor/trmnl/components/screen.blade.php | 4 ++-- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/composer.json b/composer.json index 555ad57..d856e75 100644 --- a/composer.json +++ b/composer.json @@ -15,7 +15,7 @@ "ext-imagick": "*", "ext-simplexml": "*", "ext-zip": "*", - "bnussbau/laravel-trmnl-blade": "2.1.*", + "bnussbau/laravel-trmnl-blade": "2.2.*", "bnussbau/trmnl-pipeline-php": "^0.6.0", "keepsuit/laravel-liquid": "^0.5.2", "laravel/fortify": "^1.30", diff --git a/composer.lock b/composer.lock index 80c106a..ec617d3 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": "324bc90c0d60675c736e4001ba845a5a", + "content-hash": "581bacf794841fc11c540e152c704d16", "packages": [ { "name": "aws/aws-crt-php", @@ -214,16 +214,16 @@ }, { "name": "bnussbau/laravel-trmnl-blade", - "version": "2.1.1", + "version": "2.2.1", "source": { "type": "git", "url": "https://github.com/bnussbau/laravel-trmnl-blade.git", - "reference": "6ad96eba917ebc30ebe550e6fce4a995e94f6b35" + "reference": "6db8a82a15ccedcaaffd3b37d0d337d276a26669" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/bnussbau/laravel-trmnl-blade/zipball/6ad96eba917ebc30ebe550e6fce4a995e94f6b35", - "reference": "6ad96eba917ebc30ebe550e6fce4a995e94f6b35", + "url": "https://api.github.com/repos/bnussbau/laravel-trmnl-blade/zipball/6db8a82a15ccedcaaffd3b37d0d337d276a26669", + "reference": "6db8a82a15ccedcaaffd3b37d0d337d276a26669", "shasum": "" }, "require": { @@ -278,7 +278,7 @@ ], "support": { "issues": "https://github.com/bnussbau/laravel-trmnl-blade/issues", - "source": "https://github.com/bnussbau/laravel-trmnl-blade/tree/2.1.1" + "source": "https://github.com/bnussbau/laravel-trmnl-blade/tree/2.2.1" }, "funding": [ { @@ -294,7 +294,7 @@ "type": "github" } ], - "time": "2026-01-29T20:40:42+00:00" + "time": "2026-02-05T17:57:37+00:00" }, { "name": "bnussbau/trmnl-pipeline-php", diff --git a/resources/views/vendor/trmnl/components/screen.blade.php b/resources/views/vendor/trmnl/components/screen.blade.php index 4234a2d..fbb4607 100644 --- a/resources/views/vendor/trmnl/components/screen.blade.php +++ b/resources/views/vendor/trmnl/components/screen.blade.php @@ -18,12 +18,12 @@ href="{{ config('trmnl-blade.framework_css_url') }}"> @else + href="{{ config('services.trmnl.base_url') }}/css/{{ config('trmnl-blade.framework_css_version') ?? config('trmnl-blade.framework_version', '2.1.0') }}/plugins.css"> @endif @if (config('trmnl-blade.framework_js_url')) @else - + @endif {{ $title ?? config('app.name') }} From 98c4d9f1bf4c1aac3755bc26acbfa8ae63d20281 Mon Sep 17 00:00:00 2001 From: Benjamin Nussbaum Date: Fri, 6 Feb 2026 18:02:44 +0100 Subject: [PATCH 081/104] docs: add trusted proxies --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 2231b24..670b62c 100644 --- a/README.md +++ b/README.md @@ -122,6 +122,7 @@ php artisan db:seed --class=ExampleRecipesSeeder | `REGISTRATION_ENABLED` | Allow user registration via Webinterface | 1 | | `SSL_MODE` | SSL Mode, if not using a Reverse Proxy ([docs](https://serversideup.net/open-source/docker-php/docs/customizing-the-image/configuring-ssl)) | `off` | | `FORCE_HTTPS` | If your server handles SSL termination, enforce HTTPS. | 0 | +| `TRUSTED_PROXIES` | If your server handles SSL termination, allow mixed mode. e.g. `"172.0.0.0/8"` or `*` | null | | `PHP_OPCACHE_ENABLE` | Enable PHP Opcache | 0 | | `TRMNL_IMAGE_URL_TIMEOUT` | How long TRMNL waits for a response on the display endpoint. (sec) | 30 | | `APP_TIMEZONE` | Default timezone, which will be used by the PHP date functions | UTC | From a57feabe95d2bc59adabfeaedc8a25a95eab1389 Mon Sep 17 00:00:00 2001 From: Benjamin Nussbaum Date: Fri, 6 Feb 2026 22:09:03 +0100 Subject: [PATCH 082/104] chore: bump trmnl-liquid-cli to 0.2.0 --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 2d761ed..5af7b33 100644 --- a/Dockerfile +++ b/Dockerfile @@ -18,7 +18,7 @@ ENV TRMNL_LIQUID_ENABLED=1 # Switch to the root user so we can do root things USER root -COPY --chown=www-data:www-data --from=bnussbau/trmnl-liquid-cli:0.1.0 /usr/local/bin/trmnl-liquid-cli /usr/local/bin/ +COPY --chown=www-data:www-data --from=bnussbau/trmnl-liquid-cli:0.2.0 /usr/local/bin/trmnl-liquid-cli /usr/local/bin/ # Set the working directory WORKDIR /var/www/html From 7ebfa586c1a8588f21802a9b83aebbb8a466dc3a Mon Sep 17 00:00:00 2001 From: Benjamin Nussbaum Date: Wed, 28 Jan 2026 16:02:14 +0100 Subject: [PATCH 083/104] feat: support additional markup layouts --- app/Models/Plugin.php | 44 +++- app/Services/PluginExportService.php | 58 +++-- app/Services/PluginImportService.php | 244 ++++++++++++++---- ...layout_markup_columns_to_plugins_table.php | 38 +++ .../views/livewire/plugins/index.blade.php | 2 - .../views/livewire/plugins/recipe.blade.php | 221 +++++++++++++--- tests/Feature/PluginImportTest.php | 26 +- 7 files changed, 505 insertions(+), 128 deletions(-) create mode 100644 database/migrations/2026_01_28_143142_add_layout_markup_columns_to_plugins_table.php diff --git a/app/Models/Plugin.php b/app/Models/Plugin.php index bc46559..5eeeb6b 100644 --- a/app/Models/Plugin.php +++ b/app/Models/Plugin.php @@ -60,8 +60,14 @@ class Plugin extends Model }); static::updating(function ($model): void { - // Reset image cache when markup changes - if ($model->isDirty('render_markup')) { + // Reset image cache when any markup changes + if ($model->isDirty([ + 'render_markup', + 'render_markup_half_horizontal', + 'render_markup_half_vertical', + 'render_markup_quadrant', + 'render_markup_shared', + ])) { $model->current_image = null; } }); @@ -421,7 +427,9 @@ class Plugin extends Model throw new InvalidArgumentException('Render method is only applicable for recipe plugins.'); } - if ($this->render_markup) { + $markup = $this->getMarkupForSize($size); + + if ($markup) { $renderedContent = ''; if ($this->markup_language === 'liquid') { @@ -471,7 +479,7 @@ class Plugin extends Model // Check if external renderer should be used if ($this->preferred_renderer === 'trmnl-liquid' && config('services.trmnl.liquid_enabled')) { // Use external Ruby renderer - pass raw template without preprocessing - $renderedContent = $this->renderWithExternalLiquidRenderer($this->render_markup, $context); + $renderedContent = $this->renderWithExternalLiquidRenderer($markup, $context); } else { // Use PHP keepsuit/liquid renderer // Create a custom environment with inline templates support @@ -493,14 +501,14 @@ class Plugin extends Model $environment->tagRegistry->register(TemplateTag::class); // Apply Liquid replacements (including 'with' syntax conversion) - $processedMarkup = $this->applyLiquidReplacements($this->render_markup); + $processedMarkup = $this->applyLiquidReplacements($markup); $template = $environment->parseString($processedMarkup); $liquidContext = $environment->newRenderContext(data: $context); $renderedContent = $template->render($liquidContext); } } else { - $renderedContent = Blade::render($this->render_markup, [ + $renderedContent = Blade::render($markup, [ 'size' => $size, 'data' => $this->data_payload, 'config' => $this->configuration ?? [], @@ -581,6 +589,30 @@ class Plugin extends Model return $this->configuration[$key] ?? $default; } + /** + * Get the appropriate markup for a given size, including shared prepending logic + * + * @param string $size The layout size (full, half_horizontal, half_vertical, quadrant) + * @return string|null The markup code for the given size, with shared prepended if available + */ + public function getMarkupForSize(string $size): ?string + { + $markup = match ($size) { + 'full' => $this->render_markup, + 'half_horizontal' => $this->render_markup_half_horizontal ?? $this->render_markup, + 'half_vertical' => $this->render_markup_half_vertical ?? $this->render_markup, + 'quadrant' => $this->render_markup_quadrant ?? $this->render_markup, + default => $this->render_markup, + }; + + // Prepend shared markup if it exists + if ($markup && $this->render_markup_shared) { + $markup = $this->render_markup_shared."\n".$markup; + } + + return $markup; + } + public function getPreviewMashupLayoutForSize(string $size): string { return match ($size) { diff --git a/app/Services/PluginExportService.php b/app/Services/PluginExportService.php index 241764d..be98461 100644 --- a/app/Services/PluginExportService.php +++ b/app/Services/PluginExportService.php @@ -51,17 +51,35 @@ class PluginExportService $settings = $this->generateSettingsYaml($plugin); $settingsYaml = Yaml::dump($settings, 10, 2, Yaml::DUMP_MULTI_LINE_LITERAL_BLOCK); File::put($tempDir.'/settings.yml', $settingsYaml); - // Generate full template content - $fullTemplate = $this->generateFullTemplate($plugin); + $extension = $plugin->markup_language === 'liquid' ? 'liquid' : 'blade.php'; - File::put($tempDir.'/full.'.$extension, $fullTemplate); - // Generate shared.liquid if needed (for liquid templates) - if ($plugin->markup_language === 'liquid') { - $sharedTemplate = $this->generateSharedTemplate(); - /** @phpstan-ignore-next-line */ - if ($sharedTemplate) { - File::put($tempDir.'/shared.liquid', $sharedTemplate); - } + + // Export full template if it exists + if ($plugin->render_markup) { + $fullTemplate = $this->generateLayoutTemplate($plugin->render_markup); + File::put($tempDir.'/full.'.$extension, $fullTemplate); + } + + // Export layout-specific templates if they exist + if ($plugin->render_markup_half_horizontal) { + $halfHorizontalTemplate = $this->generateLayoutTemplate($plugin->render_markup_half_horizontal); + File::put($tempDir.'/half_horizontal.'.$extension, $halfHorizontalTemplate); + } + + if ($plugin->render_markup_half_vertical) { + $halfVerticalTemplate = $this->generateLayoutTemplate($plugin->render_markup_half_vertical); + File::put($tempDir.'/half_vertical.'.$extension, $halfVerticalTemplate); + } + + if ($plugin->render_markup_quadrant) { + $quadrantTemplate = $this->generateLayoutTemplate($plugin->render_markup_quadrant); + File::put($tempDir.'/quadrant.'.$extension, $quadrantTemplate); + } + + // Export shared template if it exists + if ($plugin->render_markup_shared) { + $sharedTemplate = $this->generateLayoutTemplate($plugin->render_markup_shared); + File::put($tempDir.'/shared.'.$extension, $sharedTemplate); } // Create ZIP file $zipPath = $tempDir.'/plugin_'.$plugin->trmnlp_id.'.zip'; @@ -124,29 +142,21 @@ class PluginExportService } /** - * Generate the full template content + * Generate template content from markup, removing wrapper divs if present */ - private function generateFullTemplate(Plugin $plugin): string + private function generateLayoutTemplate(?string $markup): string { - $markup = $plugin->render_markup; + if (! $markup) { + return ''; + } - // Remove the wrapper div if it exists (it will be added during import) + // Remove the wrapper div if it exists (it will be added during import for liquid) $markup = preg_replace('/^
\s*/', '', $markup); $markup = preg_replace('/\s*<\/div>\s*$/', '', $markup); return mb_trim($markup); } - /** - * Generate the shared template content (for liquid templates) - */ - private function generateSharedTemplate(): null - { - // For now, we don't have a way to store shared templates separately - // TODO - add support for shared templates - return null; - } - /** * Add a directory and its contents to a ZIP file */ diff --git a/app/Services/PluginImportService.php b/app/Services/PluginImportService.php index 51a9aee..f3e7a5c 100644 --- a/app/Services/PluginImportService.php +++ b/app/Services/PluginImportService.php @@ -93,37 +93,59 @@ class PluginImportService $settings = Yaml::parse($settingsYaml); $this->validateYAML($settings); - // Determine which template file to use and read its content - $templatePath = null; + // Determine markup language from the first available file $markupLanguage = 'blade'; + $firstTemplatePath = $filePaths['fullLiquidPath'] + ?? ($filePaths['halfHorizontalLiquidPath'] ?? null) + ?? ($filePaths['halfVerticalLiquidPath'] ?? null) + ?? ($filePaths['quadrantLiquidPath'] ?? null) + ?? ($filePaths['sharedLiquidPath'] ?? null) + ?? ($filePaths['sharedBladePath'] ?? null); - if ($filePaths['fullLiquidPath']) { - $templatePath = $filePaths['fullLiquidPath']; - $fullLiquid = File::get($templatePath); + if ($firstTemplatePath && pathinfo((string) $firstTemplatePath, PATHINFO_EXTENSION) === 'liquid') { + $markupLanguage = 'liquid'; + } - // Prepend shared.liquid or shared.blade.php content if available - if ($filePaths['sharedLiquidPath'] && File::exists($filePaths['sharedLiquidPath'])) { - $sharedLiquid = File::get($filePaths['sharedLiquidPath']); - $fullLiquid = $sharedLiquid."\n".$fullLiquid; - } elseif ($filePaths['sharedBladePath'] && File::exists($filePaths['sharedBladePath'])) { - $sharedBlade = File::get($filePaths['sharedBladePath']); - $fullLiquid = $sharedBlade."\n".$fullLiquid; - } - - // Check if the file ends with .liquid to set markup language - if (pathinfo((string) $templatePath, PATHINFO_EXTENSION) === 'liquid') { - $markupLanguage = 'liquid'; + // Read full markup (don't prepend shared - it will be prepended at render time) + $fullLiquid = null; + if (isset($filePaths['fullLiquidPath']) && $filePaths['fullLiquidPath']) { + $fullLiquid = File::get($filePaths['fullLiquidPath']); + if ($markupLanguage === 'liquid') { $fullLiquid = '
'."\n".$fullLiquid."\n".'
'; } - } elseif ($filePaths['sharedLiquidPath']) { - $templatePath = $filePaths['sharedLiquidPath']; - $fullLiquid = File::get($templatePath); - $markupLanguage = 'liquid'; - $fullLiquid = '
'."\n".$fullLiquid."\n".'
'; - } elseif ($filePaths['sharedBladePath']) { - $templatePath = $filePaths['sharedBladePath']; - $fullLiquid = File::get($templatePath); - $markupLanguage = 'blade'; + } + + // Read shared markup separately + $sharedMarkup = null; + if (isset($filePaths['sharedLiquidPath']) && $filePaths['sharedLiquidPath'] && File::exists($filePaths['sharedLiquidPath'])) { + $sharedMarkup = File::get($filePaths['sharedLiquidPath']); + } elseif (isset($filePaths['sharedBladePath']) && $filePaths['sharedBladePath'] && File::exists($filePaths['sharedBladePath'])) { + $sharedMarkup = File::get($filePaths['sharedBladePath']); + } + + // Read layout-specific markups + $halfHorizontalMarkup = null; + if (isset($filePaths['halfHorizontalLiquidPath']) && $filePaths['halfHorizontalLiquidPath'] && File::exists($filePaths['halfHorizontalLiquidPath'])) { + $halfHorizontalMarkup = File::get($filePaths['halfHorizontalLiquidPath']); + if ($markupLanguage === 'liquid') { + $halfHorizontalMarkup = '
'."\n".$halfHorizontalMarkup."\n".'
'; + } + } + + $halfVerticalMarkup = null; + if (isset($filePaths['halfVerticalLiquidPath']) && $filePaths['halfVerticalLiquidPath'] && File::exists($filePaths['halfVerticalLiquidPath'])) { + $halfVerticalMarkup = File::get($filePaths['halfVerticalLiquidPath']); + if ($markupLanguage === 'liquid') { + $halfVerticalMarkup = '
'."\n".$halfVerticalMarkup."\n".'
'; + } + } + + $quadrantMarkup = null; + if (isset($filePaths['quadrantLiquidPath']) && $filePaths['quadrantLiquidPath'] && File::exists($filePaths['quadrantLiquidPath'])) { + $quadrantMarkup = File::get($filePaths['quadrantLiquidPath']); + if ($markupLanguage === 'liquid') { + $quadrantMarkup = '
'."\n".$quadrantMarkup."\n".'
'; + } } // Ensure custom_fields is properly formatted @@ -160,6 +182,10 @@ class PluginImportService 'polling_body' => $settings['polling_body'] ?? null, 'markup_language' => $markupLanguage, 'render_markup' => $fullLiquid ?? null, + 'render_markup_half_horizontal' => $halfHorizontalMarkup, + 'render_markup_half_vertical' => $halfVerticalMarkup, + 'render_markup_quadrant' => $quadrantMarkup, + 'render_markup_shared' => $sharedMarkup, 'configuration_template' => $configurationTemplate, 'data_payload' => json_decode($settings['static_data'] ?? '{}', true), ]); @@ -246,37 +272,59 @@ class PluginImportService $settings = Yaml::parse($settingsYaml); $this->validateYAML($settings); - // Determine which template file to use and read its content - $templatePath = null; + // Determine markup language from the first available file $markupLanguage = 'blade'; + $firstTemplatePath = $filePaths['fullLiquidPath'] + ?? ($filePaths['halfHorizontalLiquidPath'] ?? null) + ?? ($filePaths['halfVerticalLiquidPath'] ?? null) + ?? ($filePaths['quadrantLiquidPath'] ?? null) + ?? ($filePaths['sharedLiquidPath'] ?? null) + ?? ($filePaths['sharedBladePath'] ?? null); - if ($filePaths['fullLiquidPath']) { - $templatePath = $filePaths['fullLiquidPath']; - $fullLiquid = File::get($templatePath); + if ($firstTemplatePath && pathinfo((string) $firstTemplatePath, PATHINFO_EXTENSION) === 'liquid') { + $markupLanguage = 'liquid'; + } - // Prepend shared.liquid or shared.blade.php content if available - if ($filePaths['sharedLiquidPath'] && File::exists($filePaths['sharedLiquidPath'])) { - $sharedLiquid = File::get($filePaths['sharedLiquidPath']); - $fullLiquid = $sharedLiquid."\n".$fullLiquid; - } elseif ($filePaths['sharedBladePath'] && File::exists($filePaths['sharedBladePath'])) { - $sharedBlade = File::get($filePaths['sharedBladePath']); - $fullLiquid = $sharedBlade."\n".$fullLiquid; - } - - // Check if the file ends with .liquid to set markup language - if (pathinfo((string) $templatePath, PATHINFO_EXTENSION) === 'liquid') { - $markupLanguage = 'liquid'; + // Read full markup (don't prepend shared - it will be prepended at render time) + $fullLiquid = null; + if (isset($filePaths['fullLiquidPath']) && $filePaths['fullLiquidPath']) { + $fullLiquid = File::get($filePaths['fullLiquidPath']); + if ($markupLanguage === 'liquid') { $fullLiquid = '
'."\n".$fullLiquid."\n".'
'; } - } elseif ($filePaths['sharedLiquidPath']) { - $templatePath = $filePaths['sharedLiquidPath']; - $fullLiquid = File::get($templatePath); - $markupLanguage = 'liquid'; - $fullLiquid = '
'."\n".$fullLiquid."\n".'
'; - } elseif ($filePaths['sharedBladePath']) { - $templatePath = $filePaths['sharedBladePath']; - $fullLiquid = File::get($templatePath); - $markupLanguage = 'blade'; + } + + // Read shared markup separately + $sharedMarkup = null; + if (isset($filePaths['sharedLiquidPath']) && $filePaths['sharedLiquidPath'] && File::exists($filePaths['sharedLiquidPath'])) { + $sharedMarkup = File::get($filePaths['sharedLiquidPath']); + } elseif (isset($filePaths['sharedBladePath']) && $filePaths['sharedBladePath'] && File::exists($filePaths['sharedBladePath'])) { + $sharedMarkup = File::get($filePaths['sharedBladePath']); + } + + // Read layout-specific markups + $halfHorizontalMarkup = null; + if (isset($filePaths['halfHorizontalLiquidPath']) && $filePaths['halfHorizontalLiquidPath'] && File::exists($filePaths['halfHorizontalLiquidPath'])) { + $halfHorizontalMarkup = File::get($filePaths['halfHorizontalLiquidPath']); + if ($markupLanguage === 'liquid') { + $halfHorizontalMarkup = '
'."\n".$halfHorizontalMarkup."\n".'
'; + } + } + + $halfVerticalMarkup = null; + if (isset($filePaths['halfVerticalLiquidPath']) && $filePaths['halfVerticalLiquidPath'] && File::exists($filePaths['halfVerticalLiquidPath'])) { + $halfVerticalMarkup = File::get($filePaths['halfVerticalLiquidPath']); + if ($markupLanguage === 'liquid') { + $halfVerticalMarkup = '
'."\n".$halfVerticalMarkup."\n".'
'; + } + } + + $quadrantMarkup = null; + if (isset($filePaths['quadrantLiquidPath']) && $filePaths['quadrantLiquidPath'] && File::exists($filePaths['quadrantLiquidPath'])) { + $quadrantMarkup = File::get($filePaths['quadrantLiquidPath']); + if ($markupLanguage === 'liquid') { + $quadrantMarkup = '
'."\n".$quadrantMarkup."\n".'
'; + } } // Ensure custom_fields is properly formatted @@ -322,6 +370,10 @@ class PluginImportService 'polling_body' => $settings['polling_body'] ?? null, 'markup_language' => $markupLanguage, 'render_markup' => $fullLiquid ?? null, + 'render_markup_half_horizontal' => $halfHorizontalMarkup, + 'render_markup_half_vertical' => $halfVerticalMarkup, + 'render_markup_quadrant' => $quadrantMarkup, + 'render_markup_shared' => $sharedMarkup, 'configuration_template' => $configurationTemplate, 'data_payload' => json_decode($settings['static_data'] ?? '{}', true), 'preferred_renderer' => $preferredRenderer, @@ -357,6 +409,9 @@ class PluginImportService $fullLiquidPath = null; $sharedLiquidPath = null; $sharedBladePath = null; + $halfHorizontalLiquidPath = null; + $halfVerticalLiquidPath = null; + $quadrantLiquidPath = null; // If zipEntryPath is specified, look for files in that specific directory first if ($zipEntryPath) { @@ -377,6 +432,25 @@ class PluginImportService } elseif (File::exists($targetDir.'/shared.blade.php')) { $sharedBladePath = $targetDir.'/shared.blade.php'; } + + // Check for layout-specific files + if (File::exists($targetDir.'/half_horizontal.liquid')) { + $halfHorizontalLiquidPath = $targetDir.'/half_horizontal.liquid'; + } elseif (File::exists($targetDir.'/half_horizontal.blade.php')) { + $halfHorizontalLiquidPath = $targetDir.'/half_horizontal.blade.php'; + } + + if (File::exists($targetDir.'/half_vertical.liquid')) { + $halfVerticalLiquidPath = $targetDir.'/half_vertical.liquid'; + } elseif (File::exists($targetDir.'/half_vertical.blade.php')) { + $halfVerticalLiquidPath = $targetDir.'/half_vertical.blade.php'; + } + + if (File::exists($targetDir.'/quadrant.liquid')) { + $quadrantLiquidPath = $targetDir.'/quadrant.liquid'; + } elseif (File::exists($targetDir.'/quadrant.blade.php')) { + $quadrantLiquidPath = $targetDir.'/quadrant.blade.php'; + } } // Check if files are in src subdirectory of target directory @@ -394,6 +468,25 @@ class PluginImportService } elseif (File::exists($targetDir.'/src/shared.blade.php')) { $sharedBladePath = $targetDir.'/src/shared.blade.php'; } + + // Check for layout-specific files in src + if (File::exists($targetDir.'/src/half_horizontal.liquid')) { + $halfHorizontalLiquidPath = $targetDir.'/src/half_horizontal.liquid'; + } elseif (File::exists($targetDir.'/src/half_horizontal.blade.php')) { + $halfHorizontalLiquidPath = $targetDir.'/src/half_horizontal.blade.php'; + } + + if (File::exists($targetDir.'/src/half_vertical.liquid')) { + $halfVerticalLiquidPath = $targetDir.'/src/half_vertical.liquid'; + } elseif (File::exists($targetDir.'/src/half_vertical.blade.php')) { + $halfVerticalLiquidPath = $targetDir.'/src/half_vertical.blade.php'; + } + + if (File::exists($targetDir.'/src/quadrant.liquid')) { + $quadrantLiquidPath = $targetDir.'/src/quadrant.liquid'; + } elseif (File::exists($targetDir.'/src/quadrant.blade.php')) { + $quadrantLiquidPath = $targetDir.'/src/quadrant.blade.php'; + } } // If we found the required files in the target directory, return them @@ -425,6 +518,25 @@ class PluginImportService } elseif (File::exists($tempDir.'/src/shared.blade.php')) { $sharedBladePath = $tempDir.'/src/shared.blade.php'; } + + // Check for layout-specific files + if (File::exists($tempDir.'/src/half_horizontal.liquid')) { + $halfHorizontalLiquidPath = $tempDir.'/src/half_horizontal.liquid'; + } elseif (File::exists($tempDir.'/src/half_horizontal.blade.php')) { + $halfHorizontalLiquidPath = $tempDir.'/src/half_horizontal.blade.php'; + } + + if (File::exists($tempDir.'/src/half_vertical.liquid')) { + $halfVerticalLiquidPath = $tempDir.'/src/half_vertical.liquid'; + } elseif (File::exists($tempDir.'/src/half_vertical.blade.php')) { + $halfVerticalLiquidPath = $tempDir.'/src/half_vertical.blade.php'; + } + + if (File::exists($tempDir.'/src/quadrant.liquid')) { + $quadrantLiquidPath = $tempDir.'/src/quadrant.liquid'; + } elseif (File::exists($tempDir.'/src/quadrant.blade.php')) { + $quadrantLiquidPath = $tempDir.'/src/quadrant.blade.php'; + } } else { // Search for the files in the extracted directory structure $directories = new RecursiveDirectoryIterator($tempDir, RecursiveDirectoryIterator::SKIP_DOTS); @@ -442,6 +554,12 @@ class PluginImportService $sharedLiquidPath = $filepath; } elseif ($filename === 'shared.blade.php') { $sharedBladePath = $filepath; + } elseif ($filename === 'half_horizontal.liquid' || $filename === 'half_horizontal.blade.php') { + $halfHorizontalLiquidPath = $filepath; + } elseif ($filename === 'half_vertical.liquid' || $filename === 'half_vertical.blade.php') { + $halfVerticalLiquidPath = $filepath; + } elseif ($filename === 'quadrant.liquid' || $filename === 'quadrant.blade.php') { + $quadrantLiquidPath = $filepath; } } @@ -485,6 +603,25 @@ class PluginImportService $sharedBladePath = $newSrcDir.'/shared.blade.php'; } + // Copy layout-specific files if they exist + if ($halfHorizontalLiquidPath) { + $extension = pathinfo((string) $halfHorizontalLiquidPath, PATHINFO_EXTENSION); + File::copy($halfHorizontalLiquidPath, $newSrcDir.'/half_horizontal.'.$extension); + $halfHorizontalLiquidPath = $newSrcDir.'/half_horizontal.'.$extension; + } + + if ($halfVerticalLiquidPath) { + $extension = pathinfo((string) $halfVerticalLiquidPath, PATHINFO_EXTENSION); + File::copy($halfVerticalLiquidPath, $newSrcDir.'/half_vertical.'.$extension); + $halfVerticalLiquidPath = $newSrcDir.'/half_vertical.'.$extension; + } + + if ($quadrantLiquidPath) { + $extension = pathinfo((string) $quadrantLiquidPath, PATHINFO_EXTENSION); + File::copy($quadrantLiquidPath, $newSrcDir.'/quadrant.'.$extension); + $quadrantLiquidPath = $newSrcDir.'/quadrant.'.$extension; + } + // Update the paths $settingsYamlPath = $newSrcDir.'/settings.yml'; } @@ -496,6 +633,9 @@ class PluginImportService 'fullLiquidPath' => $fullLiquidPath, 'sharedLiquidPath' => $sharedLiquidPath, 'sharedBladePath' => $sharedBladePath, + 'halfHorizontalLiquidPath' => $halfHorizontalLiquidPath, + 'halfVerticalLiquidPath' => $halfVerticalLiquidPath, + 'quadrantLiquidPath' => $quadrantLiquidPath, ]; } diff --git a/database/migrations/2026_01_28_143142_add_layout_markup_columns_to_plugins_table.php b/database/migrations/2026_01_28_143142_add_layout_markup_columns_to_plugins_table.php new file mode 100644 index 0000000..e56751c --- /dev/null +++ b/database/migrations/2026_01_28_143142_add_layout_markup_columns_to_plugins_table.php @@ -0,0 +1,38 @@ +text('render_markup_half_horizontal')->nullable()->after('render_markup'); + $table->text('render_markup_half_vertical')->nullable()->after('render_markup_half_horizontal'); + $table->text('render_markup_quadrant')->nullable()->after('render_markup_half_vertical'); + $table->text('render_markup_shared')->nullable()->after('render_markup_quadrant'); + $table->text('transform_code')->nullable()->after('render_markup_shared'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('plugins', function (Blueprint $table) { + $table->dropColumn([ + 'render_markup_half_horizontal', + 'render_markup_half_vertical', + 'render_markup_quadrant', + 'render_markup_shared', + 'transform_code', + ]); + }); + } +}; diff --git a/resources/views/livewire/plugins/index.blade.php b/resources/views/livewire/plugins/index.blade.php index 90f8aa0..848fc67 100644 --- a/resources/views/livewire/plugins/index.blade.php +++ b/resources/views/livewire/plugins/index.blade.php @@ -258,7 +258,6 @@ new class extends Component
Limitations
    -
  • Only full view will be imported; shared markup will be prepended
  • Some Liquid filters may be not supported or behave differently
  • API responses in formats other than JSON are not yet supported
  • {{--
      --}} @@ -312,7 +311,6 @@ new class extends Component Limitations
        -
      • Only full view will be imported; shared markup will be prepended
      • Requires trmnl-liquid-cli executable.
      • API responses in formats other than JSON are not yet fully supported.
      • There are limitations in payload size (Data Payload, Template).
      • diff --git a/resources/views/livewire/plugins/recipe.blade.php b/resources/views/livewire/plugins/recipe.blade.php index cda019e..a7b3918 100644 --- a/resources/views/livewire/plugins/recipe.blade.php +++ b/resources/views/livewire/plugins/recipe.blade.php @@ -65,6 +65,12 @@ new class extends Component public string $preview_size = 'full'; + public array $markup_layouts = []; + + public array $active_tabs = []; + + public string $active_tab = 'full'; + public function mount(): void { abort_unless(auth()->user()->plugins->contains($this->plugin), 403); @@ -91,7 +97,24 @@ new class extends Component $this->view_content = null; } } else { - $this->markup_code = $this->plugin->render_markup; + // Initialize layout markups from plugin columns + $this->markup_layouts = [ + 'full' => $this->plugin->render_markup ?? '', + 'half_horizontal' => $this->plugin->render_markup_half_horizontal ?? '', + 'half_vertical' => $this->plugin->render_markup_half_vertical ?? '', + 'quadrant' => $this->plugin->render_markup_quadrant ?? '', + 'shared' => $this->plugin->render_markup_shared ?? '', + ]; + + // Set active tabs based on which layouts have content + $this->active_tabs = ['full']; // Full is always active + foreach (['half_horizontal', 'half_vertical', 'quadrant', 'shared'] as $layout) { + if (! empty($this->markup_layouts[$layout])) { + $this->active_tabs[] = $layout; + } + } + + $this->markup_code = $this->markup_layouts['full']; $this->markup_language = $this->plugin->markup_language ?? 'blade'; } @@ -125,12 +148,108 @@ new class extends Component { abort_unless(auth()->user()->plugins->contains($this->plugin), 403); $this->validate(); + + // Update markup_code for the active tab + if (isset($this->markup_layouts[$this->active_tab])) { + $this->markup_layouts[$this->active_tab] = $this->markup_code ?? ''; + } + + // Save all layout markups to respective columns $this->plugin->update([ - 'render_markup' => $this->markup_code ?? null, + 'render_markup' => $this->markup_layouts['full'] ?? null, + 'render_markup_half_horizontal' => ! empty($this->markup_layouts['half_horizontal']) ? $this->markup_layouts['half_horizontal'] : null, + 'render_markup_half_vertical' => ! empty($this->markup_layouts['half_vertical']) ? $this->markup_layouts['half_vertical'] : null, + 'render_markup_quadrant' => ! empty($this->markup_layouts['quadrant']) ? $this->markup_layouts['quadrant'] : null, + 'render_markup_shared' => ! empty($this->markup_layouts['shared']) ? $this->markup_layouts['shared'] : null, 'markup_language' => $this->markup_language ?? null, ]); } + public function addLayoutTab(string $layout): void + { + if (! in_array($layout, $this->active_tabs, true)) { + $this->active_tabs[] = $layout; + if (! isset($this->markup_layouts[$layout])) { + $this->markup_layouts[$layout] = ''; + } + $this->switchTab($layout); + } + } + + public function removeLayoutTab(string $layout): void + { + if ($layout !== 'full') { + $this->active_tabs = array_values(array_filter($this->active_tabs, fn ($tab) => $tab !== $layout)); + if (isset($this->markup_layouts[$layout])) { + $this->markup_layouts[$layout] = ''; + } + if ($this->active_tab === $layout) { + $this->active_tab = 'full'; + $this->markup_code = $this->markup_layouts['full'] ?? ''; + } + } + } + + public function switchTab(string $layout): void + { + if (in_array($layout, $this->active_tabs, true)) { + // Save current tab's content before switching + if (isset($this->markup_layouts[$this->active_tab])) { + $this->markup_layouts[$this->active_tab] = $this->markup_code ?? ''; + } + + $this->active_tab = $layout; + $this->markup_code = $this->markup_layouts[$layout] ?? ''; + } + } + + public function toggleLayoutTab(string $layout): void + { + if ($layout === 'full') { + return; + } + + if (in_array($layout, $this->active_tabs, true)) { + $this->removeLayoutTab($layout); + } else { + $this->addLayoutTab($layout); + } + } + + public function getAvailableLayouts(): array + { + return [ + 'half_horizontal' => 'Half Horizontal', + 'half_vertical' => 'Half Vertical', + 'quadrant' => 'Quadrant', + 'shared' => 'Shared', + ]; + } + + public function getLayoutLabel(string $layout): string + { + return match ($layout) { + 'full' => $this->getFullTabLabel(), + 'half_horizontal' => 'Half Horizontal', + 'half_vertical' => 'Half Vertical', + 'quadrant' => 'Quadrant', + 'shared' => 'Shared', + default => ucfirst($layout), + }; + } + + public function getFullTabLabel(): string + { + // Return "Full" if any layout-specific markup exists, otherwise "Responsive" + if (! empty($this->markup_layouts['half_horizontal']) + || ! empty($this->markup_layouts['half_vertical']) + || ! empty($this->markup_layouts['quadrant'])) { + return 'Full'; + } + + return 'Responsive'; + } + protected array $rules = [ 'name' => 'required|string|max:255', 'data_stale_minutes' => 'required|integer|min:1', @@ -1018,41 +1137,75 @@ HTML; @if(!$plugin->render_markup_view)
        - - @php - $textareaId = 'code-' . uniqid(); - @endphp - {{ $markup_language === 'liquid' ? 'Liquid Code' : 'Blade Code' }} -