From bcfc62c7824b9fbce7ec0ef7191ac4d936819b26 Mon Sep 17 00:00:00 2001 From: Benjamin Nussbaum Date: Sat, 31 Jan 2026 17:10:38 +0100 Subject: [PATCH 01/60] 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 02/60] 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 03/60] 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 04/60] 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 05/60] 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 06/60] 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 07/60] 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 08/60] 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 09/60] 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 10/60] 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 11/60] 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
@@ -315,7 +315,7 @@ new class extends Component
  • API responses in formats other than JSON are not yet fully supported.
  • There are limitations in payload size (Data Payload, Template).
  • - Please report issues, aside from the known limitations, on GitHub. Include the recipe URL. + Please report issues, aside from the known limitations, on GitHub. Include the recipe URL.
    diff --git a/resources/views/livewire/plugins/markup.blade.php b/resources/views/livewire/plugins/markup.blade.php index 150e626..392b0b9 100644 --- a/resources/views/livewire/plugins/markup.blade.php +++ b/resources/views/livewire/plugins/markup.blade.php @@ -72,8 +72,8 @@ new class extends Component - TRMNL BYOS Laravel - “This screen was rendered by BYOS Laravel” + LaraPaper + “This screen was rendered by BYOS LaraPaper” Benjamin Nussbaum diff --git a/resources/views/partials/head.blade.php b/resources/views/partials/head.blade.php index fa0f31a..26a42a6 100644 --- a/resources/views/partials/head.blade.php +++ b/resources/views/partials/head.blade.php @@ -1,7 +1,7 @@ -{{ $title ?? 'TRMNL BYOS Laravel' }} +{{ $title ?? 'LaraPaper' }} diff --git a/resources/views/welcome.blade.php b/resources/views/welcome.blade.php index abf6a69..ba608f9 100644 --- a/resources/views/welcome.blade.php +++ b/resources/views/welcome.blade.php @@ -1,6 +1,6 @@
    - +
    @if (Route::has('login')) @@ -33,7 +33,7 @@
    @auth @if(config('app.version')) - Version: Version: {{ config('app.version') }} @endif diff --git a/tests/Feature/PixelLogoConfigTest.php b/tests/Feature/PixelLogoConfigTest.php new file mode 100644 index 0000000..ba009c9 --- /dev/null +++ b/tests/Feature/PixelLogoConfigTest.php @@ -0,0 +1,46 @@ +get('/login'); + + $response->assertStatus(200); + $response->assertSee('viewBox="0 0 1000 150"', false); +}); + +test('auth pages show heading instead of pixel logo when pixel_logo_enabled is false', function (): void { + Config::set('app.pixel_logo_enabled', false); + + $response = $this->get('/login'); + + $response->assertStatus(200); + $response->assertDontSee('viewBox="0 0 1000 150"', false); + $response->assertSee('LaraPaper'); +}); + +test('app logo shows text when pixel_logo_enabled is false', function (): void { + Config::set('app.pixel_logo_enabled', false); + + $user = User::factory()->create(); + $response = $this->actingAs($user)->get(route('dashboard')); + + $response->assertStatus(200); + $response->assertSee('LaraPaper'); + $response->assertDontSee('viewBox="0 0 1000 150"', false); +}); + +test('app logo shows pixel logo SVG when pixel_logo_enabled is true', function (): void { + Config::set('app.pixel_logo_enabled', true); + + $user = User::factory()->create(); + $response = $this->actingAs($user)->get(route('dashboard')); + + $response->assertStatus(200); + $response->assertSee('viewBox="0 0 1000 150"', false); +}); From 433bda9639e3b15fd6329a1fd64b577f723411f0 Mon Sep 17 00:00:00 2001 From: christoph Date: Sun, 8 Mar 2026 18:12:40 +0100 Subject: [PATCH 56/60] add trmnl property to Balde renderContext to follow up liquid renderContex changes --- app/Models/Plugin.php | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/app/Models/Plugin.php b/app/Models/Plugin.php index 0a39553..7090ec3 100644 --- a/app/Models/Plugin.php +++ b/app/Models/Plugin.php @@ -575,10 +575,45 @@ class Plugin extends Model $renderedContent = $template->render($liquidContext); } } else { + // Get timezone from user or fall back to app timezone + $timezone = $this->user->timezone ?? config('app.timezone'); + + // Calculate UTC offset in seconds + $utcOffset = (string) Carbon::now($timezone)->getOffset(); + $renderedContent = Blade::render($markup, [ 'size' => $size, 'data' => $this->data_payload, 'config' => $this->configuration ?? [], + 'trmnl' => [ + 'system' => [ + 'timestamp_utc' => now()->utc()->timestamp, + ], + 'user' => [ + 'utc_offset' => $utcOffset, + 'name' => $this->user->name ?? 'Unknown User', + '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, + 'dark_mode' => $this->dark_mode ? 'yes' : 'no', + 'no_screen_padding' => $this->no_bleed ? 'yes' : 'no', + 'polling_headers' => $this->polling_header, + 'polling_url' => $this->polling_url, + 'custom_fields_values' => [ + ...(is_array($this->configuration) ? $this->configuration : []), + ], + ], + ], ]); } From 7864c8b7ab39a3311afdb1ff80fe2f7e99a6e5bb Mon Sep 17 00:00:00 2001 From: Benjamin Nussbaum Date: Wed, 11 Mar 2026 06:41:17 +0100 Subject: [PATCH 57/60] chore: update dependencies --- composer.lock | 116 +++++++++++++++++++++++++------------------------- 1 file changed, 59 insertions(+), 57 deletions(-) diff --git a/composer.lock b/composer.lock index 82d7b7a..14b2fac 100644 --- a/composer.lock +++ b/composer.lock @@ -62,16 +62,16 @@ }, { "name": "aws/aws-sdk-php", - "version": "3.372.1", + "version": "3.372.3", "source": { "type": "git", "url": "https://github.com/aws/aws-sdk-php.git", - "reference": "a48ff951eaad7f038eca3e0e89f168048b99082b" + "reference": "d207d2ca972c9b10674e535dacd4a5d956a80bad" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/a48ff951eaad7f038eca3e0e89f168048b99082b", - "reference": "a48ff951eaad7f038eca3e0e89f168048b99082b", + "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/d207d2ca972c9b10674e535dacd4a5d956a80bad", + "reference": "d207d2ca972c9b10674e535dacd4a5d956a80bad", "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.372.1" + "source": "https://github.com/aws/aws-sdk-php/tree/3.372.3" }, - "time": "2026-03-06T21:27:21+00:00" + "time": "2026-03-10T18:07:21+00:00" }, { "name": "bacon/bacon-qr-code", @@ -1435,16 +1435,16 @@ }, { "name": "guzzlehttp/psr7", - "version": "2.8.0", + "version": "2.9.0", "source": { "type": "git", "url": "https://github.com/guzzle/psr7.git", - "reference": "21dc724a0583619cd1652f673303492272778051" + "reference": "7d0ed42f28e42d61352a7a79de682e5e67fec884" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/psr7/zipball/21dc724a0583619cd1652f673303492272778051", - "reference": "21dc724a0583619cd1652f673303492272778051", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/7d0ed42f28e42d61352a7a79de682e5e67fec884", + "reference": "7d0ed42f28e42d61352a7a79de682e5e67fec884", "shasum": "" }, "require": { @@ -1460,6 +1460,7 @@ "require-dev": { "bamarni/composer-bin-plugin": "^1.8.2", "http-interop/http-factory-tests": "0.9.0", + "jshttp/mime-db": "1.54.0.1", "phpunit/phpunit": "^8.5.44 || ^9.6.25" }, "suggest": { @@ -1531,7 +1532,7 @@ ], "support": { "issues": "https://github.com/guzzle/psr7/issues", - "source": "https://github.com/guzzle/psr7/tree/2.8.0" + "source": "https://github.com/guzzle/psr7/tree/2.9.0" }, "funding": [ { @@ -1547,7 +1548,7 @@ "type": "tidelift" } ], - "time": "2025-08-23T21:21:41+00:00" + "time": "2026-03-10T16:41:02+00:00" }, { "name": "guzzlehttp/uri-template", @@ -1831,16 +1832,16 @@ }, { "name": "laravel/fortify", - "version": "v1.35.0", + "version": "v1.36.1", "source": { "type": "git", "url": "https://github.com/laravel/fortify.git", - "reference": "24c5bb81ea4787e0865c4a62f054ed7d1cb7a093" + "reference": "cad8bfeb63f6818f173d40090725c565c92651d4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/fortify/zipball/24c5bb81ea4787e0865c4a62f054ed7d1cb7a093", - "reference": "24c5bb81ea4787e0865c4a62f054ed7d1cb7a093", + "url": "https://api.github.com/repos/laravel/fortify/zipball/cad8bfeb63f6818f173d40090725c565c92651d4", + "reference": "cad8bfeb63f6818f173d40090725c565c92651d4", "shasum": "" }, "require": { @@ -1890,20 +1891,20 @@ "issues": "https://github.com/laravel/fortify/issues", "source": "https://github.com/laravel/fortify" }, - "time": "2026-02-24T14:00:44+00:00" + "time": "2026-03-10T19:59:49+00:00" }, { "name": "laravel/framework", - "version": "v12.53.0", + "version": "v12.54.1", "source": { "type": "git", "url": "https://github.com/laravel/framework.git", - "reference": "f57f035c0d34503d9ff30be76159bb35a003cd1f" + "reference": "325497463e7599cd14224c422c6e5dd2fe832868" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/framework/zipball/f57f035c0d34503d9ff30be76159bb35a003cd1f", - "reference": "f57f035c0d34503d9ff30be76159bb35a003cd1f", + "url": "https://api.github.com/repos/laravel/framework/zipball/325497463e7599cd14224c422c6e5dd2fe832868", + "reference": "325497463e7599cd14224c422c6e5dd2fe832868", "shasum": "" }, "require": { @@ -1924,7 +1925,7 @@ "guzzlehttp/uri-template": "^1.0", "laravel/prompts": "^0.3.0", "laravel/serializable-closure": "^1.3|^2.0", - "league/commonmark": "^2.7", + "league/commonmark": "^2.8.1", "league/flysystem": "^3.25.1", "league/flysystem-local": "^3.25.1", "league/uri": "^7.5.1", @@ -2112,20 +2113,20 @@ "issues": "https://github.com/laravel/framework/issues", "source": "https://github.com/laravel/framework" }, - "time": "2026-02-24T14:35:15+00:00" + "time": "2026-03-10T20:25:56+00:00" }, { "name": "laravel/prompts", - "version": "v0.3.13", + "version": "v0.3.14", "source": { "type": "git", "url": "https://github.com/laravel/prompts.git", - "reference": "ed8c466571b37e977532fb2fd3c272c784d7050d" + "reference": "9f0e371244eedfe2ebeaa72c79c54bb5df6e0176" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/prompts/zipball/ed8c466571b37e977532fb2fd3c272c784d7050d", - "reference": "ed8c466571b37e977532fb2fd3c272c784d7050d", + "url": "https://api.github.com/repos/laravel/prompts/zipball/9f0e371244eedfe2ebeaa72c79c54bb5df6e0176", + "reference": "9f0e371244eedfe2ebeaa72c79c54bb5df6e0176", "shasum": "" }, "require": { @@ -2169,9 +2170,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.13" + "source": "https://github.com/laravel/prompts/tree/v0.3.14" }, - "time": "2026-02-06T12:17:10+00:00" + "time": "2026-03-01T09:02:38+00:00" }, { "name": "laravel/sanctum", @@ -2299,16 +2300,16 @@ }, { "name": "laravel/socialite", - "version": "v5.24.3", + "version": "v5.25.0", "source": { "type": "git", "url": "https://github.com/laravel/socialite.git", - "reference": "0feb62267e7b8abc68593ca37639ad302728c129" + "reference": "231f572e1a37c9ca1fb8085e9fb8608285beafb3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/socialite/zipball/0feb62267e7b8abc68593ca37639ad302728c129", - "reference": "0feb62267e7b8abc68593ca37639ad302728c129", + "url": "https://api.github.com/repos/laravel/socialite/zipball/231f572e1a37c9ca1fb8085e9fb8608285beafb3", + "reference": "231f572e1a37c9ca1fb8085e9fb8608285beafb3", "shasum": "" }, "require": { @@ -2367,7 +2368,7 @@ "issues": "https://github.com/laravel/socialite/issues", "source": "https://github.com/laravel/socialite" }, - "time": "2026-02-21T13:32:50+00:00" + "time": "2026-02-27T13:56:35+00:00" }, { "name": "laravel/tinker", @@ -9135,16 +9136,16 @@ }, { "name": "laravel/mcp", - "version": "v0.6.0", + "version": "v0.6.2", "source": { "type": "git", "url": "https://github.com/laravel/mcp.git", - "reference": "28860a10ca0cc5433e25d897ba7af844e6c7b6a2" + "reference": "f696e44735b95ff275392eab8ce5a3b4b42a2223" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/mcp/zipball/28860a10ca0cc5433e25d897ba7af844e6c7b6a2", - "reference": "28860a10ca0cc5433e25d897ba7af844e6c7b6a2", + "url": "https://api.github.com/repos/laravel/mcp/zipball/f696e44735b95ff275392eab8ce5a3b4b42a2223", + "reference": "f696e44735b95ff275392eab8ce5a3b4b42a2223", "shasum": "" }, "require": { @@ -9204,7 +9205,7 @@ "issues": "https://github.com/laravel/mcp/issues", "source": "https://github.com/laravel/mcp" }, - "time": "2026-02-24T08:43:06+00:00" + "time": "2026-03-10T20:00:23+00:00" }, { "name": "laravel/pail", @@ -9288,16 +9289,16 @@ }, { "name": "laravel/pint", - "version": "v1.27.1", + "version": "v1.28.0", "source": { "type": "git", "url": "https://github.com/laravel/pint.git", - "reference": "54cca2de13790570c7b6f0f94f37896bee4abcb5" + "reference": "1feae84bf9c1649d99ba8f7b8193bf0f09f04cc9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/pint/zipball/54cca2de13790570c7b6f0f94f37896bee4abcb5", - "reference": "54cca2de13790570c7b6f0f94f37896bee4abcb5", + "url": "https://api.github.com/repos/laravel/pint/zipball/1feae84bf9c1649d99ba8f7b8193bf0f09f04cc9", + "reference": "1feae84bf9c1649d99ba8f7b8193bf0f09f04cc9", "shasum": "" }, "require": { @@ -9308,13 +9309,14 @@ "php": "^8.2.0" }, "require-dev": { - "friendsofphp/php-cs-fixer": "^3.93.1", - "illuminate/view": "^12.51.0", - "larastan/larastan": "^3.9.2", + "friendsofphp/php-cs-fixer": "^3.94.2", + "illuminate/view": "^12.54.1", + "larastan/larastan": "^3.9.3", "laravel-zero/framework": "^12.0.5", "mockery/mockery": "^1.6.12", - "nunomaduro/termwind": "^2.3.3", - "pestphp/pest": "^3.8.5" + "nunomaduro/termwind": "^2.4.0", + "pestphp/pest": "^3.8.5", + "shipfastlabs/agent-detector": "^1.0.2" }, "bin": [ "builds/pint" @@ -9351,7 +9353,7 @@ "issues": "https://github.com/laravel/pint/issues", "source": "https://github.com/laravel/pint" }, - "time": "2026-02-10T20:00:20+00:00" + "time": "2026-03-10T20:37:18+00:00" }, { "name": "laravel/roster", @@ -9718,21 +9720,21 @@ }, { "name": "pestphp/pest", - "version": "v4.4.1", + "version": "v4.4.2", "source": { "type": "git", "url": "https://github.com/pestphp/pest.git", - "reference": "f96a1b27864b585b0b29b0ee7331176726f7e54a" + "reference": "5d42e8fe3ae1d9fdf7c9f73ee88138fd30265701" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/pestphp/pest/zipball/f96a1b27864b585b0b29b0ee7331176726f7e54a", - "reference": "f96a1b27864b585b0b29b0ee7331176726f7e54a", + "url": "https://api.github.com/repos/pestphp/pest/zipball/5d42e8fe3ae1d9fdf7c9f73ee88138fd30265701", + "reference": "5d42e8fe3ae1d9fdf7c9f73ee88138fd30265701", "shasum": "" }, "require": { "brianium/paratest": "^7.19.0", - "nunomaduro/collision": "^8.9.0", + "nunomaduro/collision": "^8.9.1", "nunomaduro/termwind": "^2.4.0", "pestphp/pest-plugin": "^4.0.0", "pestphp/pest-plugin-arch": "^4.0.0", @@ -9752,7 +9754,7 @@ "pestphp/pest-dev-tools": "^4.1.0", "pestphp/pest-plugin-browser": "^4.3.0", "pestphp/pest-plugin-type-coverage": "^4.0.3", - "psy/psysh": "^0.12.20" + "psy/psysh": "^0.12.21" }, "bin": [ "bin/pest" @@ -9818,7 +9820,7 @@ ], "support": { "issues": "https://github.com/pestphp/pest/issues", - "source": "https://github.com/pestphp/pest/tree/v4.4.1" + "source": "https://github.com/pestphp/pest/tree/v4.4.2" }, "funding": [ { @@ -9830,7 +9832,7 @@ "type": "github" } ], - "time": "2026-02-17T15:27:18+00:00" + "time": "2026-03-10T21:09:12+00:00" }, { "name": "pestphp/pest-plugin", From 5ca028b88526ca44371f7f9e414c0dc0de7dd176 Mon Sep 17 00:00:00 2001 From: Benjamin Nussbaum Date: Wed, 11 Mar 2026 06:46:14 +0100 Subject: [PATCH 58/60] chore(#155): update om/icalparser; require PHP 8.4 --- composer.json | 4 ++-- composer.lock | 26 +++++++++++++------------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/composer.json b/composer.json index feb30c8..9ff1f90 100644 --- a/composer.json +++ b/composer.json @@ -11,7 +11,7 @@ ], "license": "MIT", "require": { - "php": "^8.2", + "php": "^8.4", "ext-imagick": "*", "ext-simplexml": "*", "ext-zip": "*", @@ -25,7 +25,7 @@ "laravel/tinker": "^2.10.1", "livewire/flux": "^2.0", "livewire/livewire": "^4.0", - "om/icalparser": "^3.2", + "om/icalparser": "^4.0", "spatie/browsershot": "^5.0", "spatie/laravel-settings": "^3.6", "stevebauman/purify": "^6.3", diff --git a/composer.lock b/composer.lock index 14b2fac..e9d30cf 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": "a1b56974da6a4f33fe847dba0549a6e0", + "content-hash": "c13d003eb08beac106e67bfbeff85dd9", "packages": [ { "name": "aws/aws-crt-php", @@ -3926,32 +3926,32 @@ }, { "name": "om/icalparser", - "version": "v3.2.1", + "version": "v4.0.0", "source": { "type": "git", "url": "https://github.com/OzzyCzech/icalparser.git", - "reference": "bc7a82b12455ae9b62ce8e7f2d0273e86c931ecc" + "reference": "3e60e2edf0bdfed6c81b69dccf0b7f2852cd87a6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/OzzyCzech/icalparser/zipball/bc7a82b12455ae9b62ce8e7f2d0273e86c931ecc", - "reference": "bc7a82b12455ae9b62ce8e7f2d0273e86c931ecc", + "url": "https://api.github.com/repos/OzzyCzech/icalparser/zipball/3e60e2edf0bdfed6c81b69dccf0b7f2852cd87a6", + "reference": "3e60e2edf0bdfed6c81b69dccf0b7f2852cd87a6", "shasum": "" }, "require": { - "php": ">=8.1.0" + "php": "^8.4" }, "require-dev": { - "nette/tester": "^2.5.7" + "nette/tester": "^2.6.0" }, "suggest": { "ext-dom": "for timezone tool" }, "type": "library", "autoload": { - "classmap": [ - "src/" - ] + "psr-4": { + "om\\": "src/" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -3971,9 +3971,9 @@ ], "support": { "issues": "https://github.com/OzzyCzech/icalparser/issues", - "source": "https://github.com/OzzyCzech/icalparser/tree/v3.2.1" + "source": "https://github.com/OzzyCzech/icalparser/tree/v4.0.0" }, - "time": "2025-12-15T06:25:09+00:00" + "time": "2026-01-29T16:45:33+00:00" }, { "name": "paragonie/constant_time_encoding", @@ -12123,7 +12123,7 @@ "prefer-stable": true, "prefer-lowest": false, "platform": { - "php": "^8.2", + "php": "^8.4", "ext-imagick": "*", "ext-simplexml": "*", "ext-zip": "*" From 7301cac8ca9a60d2deba4c20a68adaadcfaf676b Mon Sep 17 00:00:00 2001 From: Benjamin Nussbaum Date: Wed, 11 Mar 2026 08:35:21 +0100 Subject: [PATCH 59/60] fix(#203): add iCal workaround where if ORGANIZER has no parameters --- .../Plugin/Parsers/IcalResponseParser.php | 7 +- tests/Feature/Plugins/Ical/IcalParserTest.php | 163 ++++++++++++++++++ 2 files changed, 169 insertions(+), 1 deletion(-) create mode 100644 tests/Feature/Plugins/Ical/IcalParserTest.php diff --git a/app/Services/Plugin/Parsers/IcalResponseParser.php b/app/Services/Plugin/Parsers/IcalResponseParser.php index c8f2b58..a1caedb 100644 --- a/app/Services/Plugin/Parsers/IcalResponseParser.php +++ b/app/Services/Plugin/Parsers/IcalResponseParser.php @@ -25,7 +25,12 @@ class IcalResponseParser implements ResponseParser } try { - $this->parser->parseString($body); + // Workaround for om/icalparser v4.0.0 bug where it fails if ORGANIZER or ATTENDEE has no parameters. + // When ORGANIZER or ATTENDEE has no parameters (no semicolon after the key), + // IcalParser::parseRow returns an empty string for $middle instead of an array, + // which causes a type error in a foreach loop in IcalParser::parseString. + $normalizedBody = preg_replace('/^(ORGANIZER|ATTENDEE):/m', '$1;CN=Unknown:', $body); + $this->parser->parseString($normalizedBody); $events = $this->parser->getEvents()->sorted()->getArrayCopy(); $windowStart = now()->subDays(7); diff --git a/tests/Feature/Plugins/Ical/IcalParserTest.php b/tests/Feature/Plugins/Ical/IcalParserTest.php new file mode 100644 index 0000000..a394274 --- /dev/null +++ b/tests/Feature/Plugins/Ical/IcalParserTest.php @@ -0,0 +1,163 @@ + Http::response($icalContent, 200, ['Content-Type' => 'text/calendar']), + ]); + + $plugin = Plugin::factory()->create([ + 'data_strategy' => 'polling', + 'polling_url' => 'https://example.com/calendar.ics', + 'polling_verb' => 'get', + ]); + + $plugin->updateDataPayload(); + $plugin->refresh(); + + expect($plugin->data_payload)->not->toHaveKey('error'); + expect($plugin->data_payload)->toHaveKey('ical'); + expect($plugin->data_payload['ical'])->toHaveCount(1); + expect($plugin->data_payload['ical'][0]['SUMMARY'])->toBe('Meeting'); + + Carbon::setTestNow(); +}); + +test('iCal plugin parses recurring events with multiple BYDAY correctly', function (): void { + // Set test now to Monday 2024-03-25 + Carbon::setTestNow(Carbon::parse('2024-03-25 12:00:00', 'UTC')); + + $icalContent = <<<'ICS' +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//Example Corp.//EN +BEGIN:VEVENT +DESCRIPTION:XXX-REDACTED +RRULE:FREQ=WEEKLY;UNTIL=20250604T220000Z;INTERVAL=1;BYDAY=TU,TH;WKST=MO +UID:040000008200E00074C5B7101A82E00800000000E07AF34F937EDA01000000000000000 + 01000000061F3E918C753424E8154B36E55452933 +SUMMARY:Recurring Meeting +DTSTART;VALUE=DATE:20240326 +DTEND;VALUE=DATE:20240327 +DTSTAMP:20240605T082436Z +CLASS:PUBLIC +STATUS:CONFIRMED +END:VEVENT +END:VCALENDAR +ICS; + + Http::fake([ + 'example.com/recurring.ics' => Http::response($icalContent, 200, ['Content-Type' => 'text/calendar']), + ]); + + $plugin = Plugin::factory()->create([ + 'data_strategy' => 'polling', + 'polling_url' => 'https://example.com/recurring.ics', + 'polling_verb' => 'get', + ]); + + $plugin->updateDataPayload(); + $plugin->refresh(); + + $ical = $plugin->data_payload['ical']; + + // Week of March 25, 2024: + // Tue March 26: 2024-03-26 (DTSTART) + // Thu March 28: 2024-03-28 (Recurrence) + + // The parser window is now-7 days to now+30 days. + // Window: 2024-03-18 to 2024-04-24. + + $summaries = collect($ical)->pluck('SUMMARY'); + expect($summaries)->toContain('Recurring Meeting'); + + $dates = collect($ical)->map(fn ($event) => Carbon::parse($event['DTSTART'])->format('Y-m-d'))->values(); + + // Check if Tuesday March 26 is present + expect($dates)->toContain('2024-03-26'); + + // Check if Thursday March 28 is present (THIS IS WHERE IT IS EXPECTED TO FAIL BASED ON THE ISSUE) + expect($dates)->toContain('2024-03-28'); + + Carbon::setTestNow(); +}); + +test('iCal plugin parses recurring events with multiple BYDAY and specific DTSTART correctly', function (): void { + // Set test now to Monday 2024-03-25 + Carbon::setTestNow(Carbon::parse('2024-03-25 12:00:00', 'UTC')); + + $icalContent = <<<'ICS' +BEGIN:VCALENDAR +VERSION:2.0 +X-WR-TIMEZONE:UTC +PRODID:-//Example Corp.//EN +BEGIN:VEVENT +RRULE:FREQ=WEEKLY;UNTIL=20250604T220000Z;INTERVAL=1;BYDAY=TU,TH;WKST=MO +UID:recurring-event-2 +SUMMARY:Recurring Meeting 2 +DTSTART:20240326T100000 +DTEND:20240326T110000 +DTSTAMP:20240605T082436Z +END:VEVENT +END:VCALENDAR +ICS; + + Http::fake([ + 'example.com/recurring2.ics' => Http::response($icalContent, 200, ['Content-Type' => 'text/calendar']), + ]); + + $plugin = Plugin::factory()->create([ + 'data_strategy' => 'polling', + 'polling_url' => 'https://example.com/recurring2.ics', + 'polling_verb' => 'get', + ]); + + $plugin->updateDataPayload(); + $plugin->refresh(); + + $ical = $plugin->data_payload['ical']; + $dates = collect($ical)->map(fn ($event) => Carbon::parse($event['DTSTART'])->format('Y-m-d'))->values(); + + expect($dates)->toContain('2024-03-26'); + expect($dates)->toContain('2024-03-28'); + + Carbon::setTestNow(); +}); From 90fbfadfa4bda82e6d8311046d6750a5da954777 Mon Sep 17 00:00:00 2001 From: Benjamin Nussbaum Date: Sat, 14 Mar 2026 19:06:52 +0100 Subject: [PATCH 60/60] chore: update dependencies --- composer.lock | 68 +++++++++++++++++++++++++-------------------------- 1 file changed, 34 insertions(+), 34 deletions(-) diff --git a/composer.lock b/composer.lock index e9d30cf..5b51acf 100644 --- a/composer.lock +++ b/composer.lock @@ -62,16 +62,16 @@ }, { "name": "aws/aws-sdk-php", - "version": "3.372.3", + "version": "3.373.2", "source": { "type": "git", "url": "https://github.com/aws/aws-sdk-php.git", - "reference": "d207d2ca972c9b10674e535dacd4a5d956a80bad" + "reference": "483fba51c28b3a0c0647bf5100e0edca82090b18" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/d207d2ca972c9b10674e535dacd4a5d956a80bad", - "reference": "d207d2ca972c9b10674e535dacd4a5d956a80bad", + "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/483fba51c28b3a0c0647bf5100e0edca82090b18", + "reference": "483fba51c28b3a0c0647bf5100e0edca82090b18", "shasum": "" }, "require": { @@ -92,12 +92,12 @@ "aws/aws-php-sns-message-validator": "~1.0", "behat/behat": "~3.0", "composer/composer": "^2.7.8", - "dms/phpunit-arraysubset-asserts": "^0.4.0", + "dms/phpunit-arraysubset-asserts": "^v0.5.0", "doctrine/cache": "~1.4", "ext-dom": "*", "ext-openssl": "*", "ext-sockets": "*", - "phpunit/phpunit": "^9.6", + "phpunit/phpunit": "^10.0", "psr/cache": "^2.0 || ^3.0", "psr/simple-cache": "^2.0 || ^3.0", "sebastian/comparator": "^1.2.3 || ^4.0 || ^5.0", @@ -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.372.3" + "source": "https://github.com/aws/aws-sdk-php/tree/3.373.2" }, - "time": "2026-03-10T18:07:21+00:00" + "time": "2026-03-13T18:08:30+00:00" }, { "name": "bacon/bacon-qr-code", @@ -3518,16 +3518,16 @@ }, { "name": "nesbot/carbon", - "version": "3.11.1", + "version": "3.11.3", "source": { "type": "git", "url": "https://github.com/CarbonPHP/carbon.git", - "reference": "f438fcc98f92babee98381d399c65336f3a3827f" + "reference": "6a7e652845bb018c668220c2a545aded8594fbbf" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/CarbonPHP/carbon/zipball/f438fcc98f92babee98381d399c65336f3a3827f", - "reference": "f438fcc98f92babee98381d399c65336f3a3827f", + "url": "https://api.github.com/repos/CarbonPHP/carbon/zipball/6a7e652845bb018c668220c2a545aded8594fbbf", + "reference": "6a7e652845bb018c668220c2a545aded8594fbbf", "shasum": "" }, "require": { @@ -3619,7 +3619,7 @@ "type": "tidelift" } ], - "time": "2026-01-29T09:26:29+00:00" + "time": "2026-03-11T17:23:39+00:00" }, { "name": "nette/schema", @@ -8452,21 +8452,21 @@ }, { "name": "wnx/sidecar-browsershot", - "version": "v2.8.0", + "version": "v2.9.0", "source": { "type": "git", "url": "https://github.com/stefanzweifel/sidecar-browsershot.git", - "reference": "1d2a20a6723b74c139f98f7a020fe5c0f57d05a5" + "reference": "352083995009bec142ff0c7ae4b2883831be0685" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/stefanzweifel/sidecar-browsershot/zipball/1d2a20a6723b74c139f98f7a020fe5c0f57d05a5", - "reference": "1d2a20a6723b74c139f98f7a020fe5c0f57d05a5", + "url": "https://api.github.com/repos/stefanzweifel/sidecar-browsershot/zipball/352083995009bec142ff0c7ae4b2883831be0685", + "reference": "352083995009bec142ff0c7ae4b2883831be0685", "shasum": "" }, "require": { - "hammerstone/sidecar": "^0.7.1", - "illuminate/contracts": "^12.0", + "hammerstone/sidecar": "^0.7.1 | ^0.8.0", + "illuminate/contracts": "^12.0 | ^13.0", "php": "^8.4", "spatie/browsershot": "^5.0", "spatie/laravel-package-tools": "^1.9.2" @@ -8476,7 +8476,7 @@ "laravel/pint": "^1.13", "league/flysystem-aws-s3-v3": "^1.0|^2.0|^3.0", "nunomaduro/collision": "^7.0|^8.0", - "orchestra/testbench": "^10.0", + "orchestra/testbench": "^10.0 | ^11.0", "pestphp/pest": "^3.0|^4.0", "pestphp/pest-plugin-laravel": "^3.0|^4.0", "phpstan/extension-installer": "^1.1", @@ -8526,7 +8526,7 @@ ], "support": { "issues": "https://github.com/stefanzweifel/sidecar-browsershot/issues", - "source": "https://github.com/stefanzweifel/sidecar-browsershot/tree/v2.8.0" + "source": "https://github.com/stefanzweifel/sidecar-browsershot/tree/v2.9.0" }, "funding": [ { @@ -8534,7 +8534,7 @@ "type": "github" } ], - "time": "2026-03-07T18:24:28+00:00" + "time": "2026-03-13T20:12:58+00:00" } ], "packages-dev": [ @@ -9070,16 +9070,16 @@ }, { "name": "laravel/boost", - "version": "v2.2.3", + "version": "v2.3.1", "source": { "type": "git", "url": "https://github.com/laravel/boost.git", - "reference": "44ab65a5455c2d6fceb71d6145f8d5d89c02d889" + "reference": "ba0a9e6497398b6ce8243f5517b67d6761509150" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/boost/zipball/44ab65a5455c2d6fceb71d6145f8d5d89c02d889", - "reference": "44ab65a5455c2d6fceb71d6145f8d5d89c02d889", + "url": "https://api.github.com/repos/laravel/boost/zipball/ba0a9e6497398b6ce8243f5517b67d6761509150", + "reference": "ba0a9e6497398b6ce8243f5517b67d6761509150", "shasum": "" }, "require": { @@ -9132,7 +9132,7 @@ "issues": "https://github.com/laravel/boost/issues", "source": "https://github.com/laravel/boost" }, - "time": "2026-03-06T20:20:28+00:00" + "time": "2026-03-12T09:06:47+00:00" }, { "name": "laravel/mcp", @@ -9289,16 +9289,16 @@ }, { "name": "laravel/pint", - "version": "v1.28.0", + "version": "v1.29.0", "source": { "type": "git", "url": "https://github.com/laravel/pint.git", - "reference": "1feae84bf9c1649d99ba8f7b8193bf0f09f04cc9" + "reference": "bdec963f53172c5e36330f3a400604c69bf02d39" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/pint/zipball/1feae84bf9c1649d99ba8f7b8193bf0f09f04cc9", - "reference": "1feae84bf9c1649d99ba8f7b8193bf0f09f04cc9", + "url": "https://api.github.com/repos/laravel/pint/zipball/bdec963f53172c5e36330f3a400604c69bf02d39", + "reference": "bdec963f53172c5e36330f3a400604c69bf02d39", "shasum": "" }, "require": { @@ -9315,8 +9315,8 @@ "laravel-zero/framework": "^12.0.5", "mockery/mockery": "^1.6.12", "nunomaduro/termwind": "^2.4.0", - "pestphp/pest": "^3.8.5", - "shipfastlabs/agent-detector": "^1.0.2" + "pestphp/pest": "^3.8.6", + "shipfastlabs/agent-detector": "^1.1.0" }, "bin": [ "builds/pint" @@ -9353,7 +9353,7 @@ "issues": "https://github.com/laravel/pint/issues", "source": "https://github.com/laravel/pint" }, - "time": "2026-03-10T20:37:18+00:00" + "time": "2026-03-12T15:51:39+00:00" }, { "name": "laravel/roster",