diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml index c8327d3..a4ff129 100644 --- a/.github/workflows/docker-build.yml +++ b/.github/workflows/docker-build.yml @@ -6,6 +6,7 @@ on: env: REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }} jobs: build-and-push: @@ -39,9 +40,7 @@ jobs: id: meta uses: docker/metadata-action@v5 with: - images: | - ${{ env.REGISTRY }}/usetrmnl/byos_laravel - ${{ env.REGISTRY }}/usetrmnl/larapaper + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} tags: | type=semver,pattern={{version}} diff --git a/Dockerfile b/Dockerfile index 4dc531b..c679db8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,8 +3,8 @@ ######################## FROM bnussbau/serversideup-php:8.4-fpm-nginx-alpine-imagick-chromium@sha256:ed705a4060d50143ddc538c1288afff217eaf76ad5791f7556a97943854cf745 AS base -LABEL org.opencontainers.image.source=https://github.com/usetrmnl/larapaper -LABEL org.opencontainers.image.description="LaraPaper" +LABEL org.opencontainers.image.source=https://github.com/usetrmnl/byos_laravel +LABEL org.opencontainers.image.description="TRMNL BYOS Laravel" LABEL org.opencontainers.image.licenses=MIT ARG APP_VERSION diff --git a/README.md b/README.md index c1ebafe..442cc15 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ -## LaraPaper (PHP/Laravel) +## TRMNL BYOS (PHP/Laravel) -[![tests](https://github.com/usetrmnl/larapaper/actions/workflows/test.yml/badge.svg)](https://github.com/usetrmnl/larapaper/actions/workflows/test.yml) +[![tests](https://github.com/usetrmnl/byos_laravel/actions/workflows/test.yml/badge.svg)](https://github.com/usetrmnl/byos_laravel/actions/workflows/test.yml) -LaraPaper is a self-hostable implementation of a TRMNL server (BYOS), built with Laravel. +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** (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) @@ -26,7 +26,7 @@ It allows you to manage TRMNL devices, generate screens using **native plugins** * Custom ESP32 with TRMNL firmware * E-Reader Devices * KOReader ([trmnl-koreader](https://github.com/usetrmnl/trmnl-koreader)) - * Kindle ([trmnl-kindle](https://github.com/usetrmnl/larapaper/pull/27)) + * Kindle ([trmnl-kindle](https://github.com/usetrmnl/byos_laravel/pull/27)) * Nook ([trmnl-nook](https://github.com/usetrmnl/trmnl-nook)) * Kobo ([trmnl-kobo](https://github.com/usetrmnl/trmnl-kobo)) * Android Devices with [trmnl-android](https://github.com/usetrmnl/trmnl-android) @@ -61,7 +61,7 @@ Docker Compose file located at: [docker/prod/docker-compose.yml](docker/prod/doc ##### Backup Database ```sh -docker ps #find container id of larapaper container +docker ps #find container id of byos_laravel container docker cp {{CONTAINER_ID}}:/var/www/html/database/storage/database.sqlite database_backup.sqlite ``` @@ -73,11 +73,11 @@ docker compose up -d ``` #### VPS -If you’re using a VPS (e.g., Hetzner) and prefer an alternative to native Docker, you can install Dokploy and deploy LaraPaper using the integrated [Template](https://templates.dokploy.com/?q=trmnl+byos+laravel). +If you’re using a VPS (e.g., Hetzner) and prefer an alternative to native Docker, you can install Dokploy and deploy BYOS Laravel using the integrated [Template](https://templates.dokploy.com/?q=trmnl+byos+laravel). It’s a quick way to get started without having to manually manage Docker setup. #### PikaPods -You can vote for LaraPaper to be included as PikaPods Template here: [feedback.pikapods.com](https://feedback.pikapods.com/posts/842/add-app-trmnl-byos-laravel) +You can vote for TRMNL BYOS Laravel to be included as PikaPods Template here: [feedback.pikapods.com](https://feedback.pikapods.com/posts/842/add-app-trmnl-byos-laravel) #### Umbrel Umbrel is supported through a community store, [see](http://github.com/bnussbau/umbrel-store). @@ -173,13 +173,13 @@ See this YouTube guide: [https://www.youtube.com/watch?v=3xehPW-PCOM](https://ww ### ☁️ Activate fresh TRMNL Device with Cloud Proxy 1) Setup the TRMNL as in the official docs with the cloud service (connect one of the plugins to later verify it works) -2) Setup LaraPaper, create a user and login -3) In LaraPaper in the header bar, activate the toggle "Permit Auto-Join" +2) Setup Laravel BYOS, create a user and login +3) In Laravel BYOS in the header bar, activate the toggle "Permit Auto-Join" 4) Press and hold the button on the back of your TRMNL for 5 seconds to reactivate the captive portal (or reflash). -5) Go through the setup process again, in the screen where you provide the Wi-Fi credentials there is also option to set the Server URL. Use the local address of LaraPaper. +5) Go through the setup process again, in the screen where you provide the Wi-Fi credentials there is also option to set the Server URL. Use the local address of your Laravel BYOS 6) The device should automatically appear in the device list; you can deactivate the "Permit Auto-Join" toggle again. 7) In the devices list, activate the toggle "☁️ Proxy" for your device. (Make sure that the queue worker is active. In the docker image it should be running automatically.) -8) As long as no LaraPaper plugin is scheduled, the device will show your cloud plugins. +8) As long as no Laravel BYOS plugin is scheduled, the device will show your cloud plugins. ###### Troubleshooting diff --git a/app/Jobs/GenerateScreenJob.php b/app/Jobs/GenerateScreenJob.php index 4af43dd..b9661cc 100644 --- a/app/Jobs/GenerateScreenJob.php +++ b/app/Jobs/GenerateScreenJob.php @@ -34,13 +34,8 @@ class GenerateScreenJob implements ShouldQueue Device::find($this->deviceId)->update(['current_screen_image' => $newImageUuid]); if ($this->pluginId) { - $plugin = Plugin::find($this->pluginId); - $update = ['current_image' => $newImageUuid]; - if ($plugin->plugin_type === 'recipe') { - $device = Device::with(['deviceModel', 'deviceModel.palette'])->find($this->deviceId); - $update['current_image_metadata'] = ImageGenerationService::buildImageMetadataFromDevice($device); - } - $plugin->update($update); + // cache current image + Plugin::find($this->pluginId)->update(['current_image' => $newImageUuid]); } ImageGenerationService::cleanupFolder(); diff --git a/app/Models/Plugin.php b/app/Models/Plugin.php index 0a39553..3f84bc5 100644 --- a/app/Models/Plugin.php +++ b/app/Models/Plugin.php @@ -49,7 +49,6 @@ class Plugin extends Model 'preferred_renderer' => 'string', 'plugin_type' => 'string', 'alias' => 'boolean', - 'current_image_metadata' => 'array', ]; protected static function boot() @@ -72,7 +71,6 @@ class Plugin extends Model 'render_markup_shared', ])) { $model->current_image = null; - $model->current_image_metadata = null; } }); @@ -224,7 +222,7 @@ class Plugin extends Model if ($this->data_strategy !== 'polling' || ! $this->polling_url) { return; } - $headers = ['User-Agent' => 'usetrmnl/larapaper', 'Accept' => 'application/json']; + $headers = ['User-Agent' => 'usetrmnl/byos_laravel', 'Accept' => 'application/json']; // resolve headers if ($this->polling_header) { diff --git a/app/Services/ImageGenerationService.php b/app/Services/ImageGenerationService.php index 75f374e..fa6f325 100644 --- a/app/Services/ImageGenerationService.php +++ b/app/Services/ImageGenerationService.php @@ -331,88 +331,36 @@ class ImageGenerationService } } - /** - * Ensure plugin image cache is valid for the current context. No-op for image_webhook. - * When deviceOrModel is provided (recipe only), clears cache if stored metadata does not match. - */ - public static function resetIfNotCacheable(?Plugin $plugin, Device|DeviceModel|null $deviceOrModel = null): void + public static function resetIfNotCacheable(?Plugin $plugin): void { - if (! $plugin?->id || $plugin->plugin_type === 'image_webhook') { - return; - } - if ($deviceOrModel === null || $plugin->plugin_type !== 'recipe') { - return; - } - if ($plugin->current_image === null) { - return; - } - if (self::imageMetadataMatches($plugin->current_image_metadata, $deviceOrModel)) { - return; - } - $plugin->update([ - 'current_image' => null, - 'current_image_metadata' => null, - ]); - Log::debug("Plugin {$plugin->id}: cleared image cache due to metadata mismatch"); - } + if ($plugin?->id) { + // Image webhook plugins have finalized images that shouldn't be reset + if ($plugin->plugin_type === 'image_webhook') { + return; + } + // Check if any devices have custom dimensions or use non-standard DeviceModels + $hasCustomDimensions = Device::query() + ->where(function ($query): void { + $query->where('width', '!=', 800) + ->orWhere('height', '!=', 480) + ->orWhere('rotate', '!=', 0); + }) + ->orWhereHas('deviceModel', function ($query): void { + // Only allow caching if all device models have standard dimensions (800x480, rotation=0) + $query->where(function ($subQuery): void { + $subQuery->where('width', '!=', 800) + ->orWhere('height', '!=', 480) + ->orWhere('rotation', '!=', 0); + }); + }) + ->exists(); - /** - * Build canonical image metadata from a Device for cache comparison. - * - * @return array{width: int, height: int, rotation: int, palette_id: int|null, mime_type: string} - */ - public static function buildImageMetadataFromDevice(Device $device): array - { - $device->loadMissing(['deviceModel', 'deviceModel.palette']); - $settings = self::getImageSettings($device); - $paletteId = $device->palette_id ?? $device->deviceModel?->palette_id; - - return [ - 'width' => $settings['width'], - 'height' => $settings['height'], - 'rotation' => $settings['rotation'] ?? 0, - 'palette_id' => $paletteId, - 'mime_type' => $settings['mime_type'], - ]; - } - - /** - * Build canonical image metadata from a DeviceModel for cache comparison. - * - * @return array{width: int, height: int, rotation: int, palette_id: int|null, mime_type: string} - */ - public static function buildImageMetadataFromDeviceModel(DeviceModel $model): array - { - return [ - 'width' => $model->width, - 'height' => $model->height, - 'rotation' => $model->rotation ?? 0, - 'palette_id' => $model->palette_id, - 'mime_type' => $model->mime_type, - ]; - } - - /** - * Check if stored metadata matches the current device or device model. - * Returns false if stored is null or empty so cache is regenerated and metadata is stored. - */ - public static function imageMetadataMatches(?array $stored, Device|DeviceModel $deviceOrModel): bool - { - if ($stored === null || $stored === []) { - return false; - } - - $current = $deviceOrModel instanceof Device - ? self::buildImageMetadataFromDevice($deviceOrModel) - : self::buildImageMetadataFromDeviceModel($deviceOrModel); - - foreach (['width', 'height', 'rotation', 'palette_id', 'mime_type'] as $key) { - if (($stored[$key] ?? null) !== ($current[$key] ?? null)) { - return false; + if ($hasCustomDimensions) { + // TODO cache image per device + $plugin->update(['current_image' => null]); + Log::debug('Skip cache as devices with custom dimensions or non-standard DeviceModels exist'); } } - - return true; } /** diff --git a/composer.lock b/composer.lock index 82d7b7a..fb63ef3 100644 --- a/composer.lock +++ b/composer.lock @@ -62,16 +62,16 @@ }, { "name": "aws/aws-sdk-php", - "version": "3.372.1", + "version": "3.372.0", "source": { "type": "git", "url": "https://github.com/aws/aws-sdk-php.git", - "reference": "a48ff951eaad7f038eca3e0e89f168048b99082b" + "reference": "91ff063599dd5775e8886b9a7ba13cb1f40ca201" }, "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/91ff063599dd5775e8886b9a7ba13cb1f40ca201", + "reference": "91ff063599dd5775e8886b9a7ba13cb1f40ca201", "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.0" }, - "time": "2026-03-06T21:27:21+00:00" + "time": "2026-03-05T19:38:44+00:00" }, { "name": "bacon/bacon-qr-code", @@ -4902,16 +4902,16 @@ }, { "name": "psy/psysh", - "version": "v0.12.21", + "version": "v0.12.20", "source": { "type": "git", "url": "https://github.com/bobthecow/psysh.git", - "reference": "4821fab5b7cd8c49a673a9fd5754dc9162bb9e97" + "reference": "19678eb6b952a03b8a1d96ecee9edba518bb0373" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/bobthecow/psysh/zipball/4821fab5b7cd8c49a673a9fd5754dc9162bb9e97", - "reference": "4821fab5b7cd8c49a673a9fd5754dc9162bb9e97", + "url": "https://api.github.com/repos/bobthecow/psysh/zipball/19678eb6b952a03b8a1d96ecee9edba518bb0373", + "reference": "19678eb6b952a03b8a1d96ecee9edba518bb0373", "shasum": "" }, "require": { @@ -4975,9 +4975,9 @@ ], "support": { "issues": "https://github.com/bobthecow/psysh/issues", - "source": "https://github.com/bobthecow/psysh/tree/v0.12.21" + "source": "https://github.com/bobthecow/psysh/tree/v0.12.20" }, - "time": "2026-03-06T21:21:28+00:00" + "time": "2026-02-11T15:05:28+00:00" }, { "name": "ralouphie/getallheaders", @@ -5597,16 +5597,16 @@ }, { "name": "symfony/console", - "version": "v7.4.7", + "version": "v7.4.6", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "e1e6770440fb9c9b0cf725f81d1361ad1835329d" + "reference": "6d643a93b47398599124022eb24d97c153c12f27" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/e1e6770440fb9c9b0cf725f81d1361ad1835329d", - "reference": "e1e6770440fb9c9b0cf725f81d1361ad1835329d", + "url": "https://api.github.com/repos/symfony/console/zipball/6d643a93b47398599124022eb24d97c153c12f27", + "reference": "6d643a93b47398599124022eb24d97c153c12f27", "shasum": "" }, "require": { @@ -5671,7 +5671,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v7.4.7" + "source": "https://github.com/symfony/console/tree/v7.4.6" }, "funding": [ { @@ -5691,7 +5691,7 @@ "type": "tidelift" } ], - "time": "2026-03-06T14:06:20+00:00" + "time": "2026-02-25T17:02:47+00:00" }, { "name": "symfony/css-selector", @@ -6212,16 +6212,16 @@ }, { "name": "symfony/http-foundation", - "version": "v7.4.7", + "version": "v7.4.6", "source": { "type": "git", "url": "https://github.com/symfony/http-foundation.git", - "reference": "f94b3e7b7dafd40e666f0c9ff2084133bae41e81" + "reference": "fd97d5e926e988a363cef56fbbf88c5c528e9065" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-foundation/zipball/f94b3e7b7dafd40e666f0c9ff2084133bae41e81", - "reference": "f94b3e7b7dafd40e666f0c9ff2084133bae41e81", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/fd97d5e926e988a363cef56fbbf88c5c528e9065", + "reference": "fd97d5e926e988a363cef56fbbf88c5c528e9065", "shasum": "" }, "require": { @@ -6270,7 +6270,7 @@ "description": "Defines an object-oriented layer for the HTTP specification", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/http-foundation/tree/v7.4.7" + "source": "https://github.com/symfony/http-foundation/tree/v7.4.6" }, "funding": [ { @@ -6290,20 +6290,20 @@ "type": "tidelift" } ], - "time": "2026-03-06T13:15:18+00:00" + "time": "2026-02-21T16:25:55+00:00" }, { "name": "symfony/http-kernel", - "version": "v7.4.7", + "version": "v7.4.6", "source": { "type": "git", "url": "https://github.com/symfony/http-kernel.git", - "reference": "3b3fcf386c809be990c922e10e4c620d6367cab1" + "reference": "002ac0cf4cd972a7fd0912dcd513a95e8a81ce83" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-kernel/zipball/3b3fcf386c809be990c922e10e4c620d6367cab1", - "reference": "3b3fcf386c809be990c922e10e4c620d6367cab1", + "url": "https://api.github.com/repos/symfony/http-kernel/zipball/002ac0cf4cd972a7fd0912dcd513a95e8a81ce83", + "reference": "002ac0cf4cd972a7fd0912dcd513a95e8a81ce83", "shasum": "" }, "require": { @@ -6389,7 +6389,7 @@ "description": "Provides a structured process for converting a Request into a Response", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/http-kernel/tree/v7.4.7" + "source": "https://github.com/symfony/http-kernel/tree/v7.4.6" }, "funding": [ { @@ -6409,7 +6409,7 @@ "type": "tidelift" } ], - "time": "2026-03-06T16:33:18+00:00" + "time": "2026-02-26T08:30:57+00:00" }, { "name": "symfony/mailer", @@ -6497,16 +6497,16 @@ }, { "name": "symfony/mime", - "version": "v7.4.7", + "version": "v7.4.6", "source": { "type": "git", "url": "https://github.com/symfony/mime.git", - "reference": "da5ab4fde3f6c88ab06e96185b9922f48b677cd1" + "reference": "9fc881d95feae4c6c48678cb6372bd8a7ba04f5f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/mime/zipball/da5ab4fde3f6c88ab06e96185b9922f48b677cd1", - "reference": "da5ab4fde3f6c88ab06e96185b9922f48b677cd1", + "url": "https://api.github.com/repos/symfony/mime/zipball/9fc881d95feae4c6c48678cb6372bd8a7ba04f5f", + "reference": "9fc881d95feae4c6c48678cb6372bd8a7ba04f5f", "shasum": "" }, "require": { @@ -6562,7 +6562,7 @@ "mime-type" ], "support": { - "source": "https://github.com/symfony/mime/tree/v7.4.7" + "source": "https://github.com/symfony/mime/tree/v7.4.6" }, "funding": [ { @@ -6582,7 +6582,7 @@ "type": "tidelift" } ], - "time": "2026-03-05T15:24:09+00:00" + "time": "2026-02-05T15:57:06+00:00" }, { "name": "symfony/polyfill-ctype", @@ -8451,23 +8451,23 @@ }, { "name": "wnx/sidecar-browsershot", - "version": "v2.8.0", + "version": "v2.7.0", "source": { "type": "git", "url": "https://github.com/stefanzweifel/sidecar-browsershot.git", - "reference": "1d2a20a6723b74c139f98f7a020fe5c0f57d05a5" + "reference": "e42a996c6fab4357919cd5e3f3fab33f019cdd80" }, "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/e42a996c6fab4357919cd5e3f3fab33f019cdd80", + "reference": "e42a996c6fab4357919cd5e3f3fab33f019cdd80", "shasum": "" }, "require": { - "hammerstone/sidecar": "^0.7.1", + "hammerstone/sidecar": "^0.7", "illuminate/contracts": "^12.0", "php": "^8.4", - "spatie/browsershot": "^5.0", + "spatie/browsershot": "^4.0 || ^5.0", "spatie/laravel-package-tools": "^1.9.2" }, "require-dev": { @@ -8483,7 +8483,7 @@ "phpstan/phpstan-phpunit": "^1.0|^2.0", "phpunit/phpunit": "^11.0 | ^12.0", "spatie/image": "^3.3", - "spatie/pixelmatch-php": "^1.2" + "spatie/pixelmatch-php": "^1.0" }, "type": "library", "extra": { @@ -8525,7 +8525,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.7.0" }, "funding": [ { @@ -8533,7 +8533,7 @@ "type": "github" } ], - "time": "2026-03-07T18:24:28+00:00" + "time": "2025-11-22T08:49:08+00:00" } ], "packages-dev": [ @@ -9069,24 +9069,24 @@ }, { "name": "laravel/boost", - "version": "v2.2.3", + "version": "v2.2.2", "source": { "type": "git", "url": "https://github.com/laravel/boost.git", - "reference": "44ab65a5455c2d6fceb71d6145f8d5d89c02d889" + "reference": "2b0366559e9ff591c65ea0321dfb91fd950c2cbd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/boost/zipball/44ab65a5455c2d6fceb71d6145f8d5d89c02d889", - "reference": "44ab65a5455c2d6fceb71d6145f8d5d89c02d889", + "url": "https://api.github.com/repos/laravel/boost/zipball/2b0366559e9ff591c65ea0321dfb91fd950c2cbd", + "reference": "2b0366559e9ff591c65ea0321dfb91fd950c2cbd", "shasum": "" }, "require": { "guzzlehttp/guzzle": "^7.9", - "illuminate/console": "^11.45.3|^12.41.1|^13.0", - "illuminate/contracts": "^11.45.3|^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/console": "^11.45.3|^12.41.1", + "illuminate/contracts": "^11.45.3|^12.41.1", + "illuminate/routing": "^11.45.3|^12.41.1", + "illuminate/support": "^11.45.3|^12.41.1", "laravel/mcp": "^0.5.1|^0.6.0", "laravel/prompts": "^0.3.10", "laravel/roster": "^0.5.0", @@ -9095,7 +9095,7 @@ "require-dev": { "laravel/pint": "^1.27.0", "mockery/mockery": "^1.6.12", - "orchestra/testbench": "^9.15.0|^10.6|^11.0", + "orchestra/testbench": "^9.15.0|^10.6", "pestphp/pest": "^2.36.0|^3.8.4|^4.1.5", "phpstan/phpstan": "^2.1.27", "rector/rector": "^2.1" @@ -9131,7 +9131,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-03T14:36:03+00:00" }, { "name": "laravel/mcp", diff --git a/config/app.php b/config/app.php index 176342e..fe8499c 100644 --- a/config/app.php +++ b/config/app.php @@ -13,7 +13,7 @@ return [ | */ - 'name' => env('APP_NAME', 'LaraPaper'), + 'name' => env('APP_NAME', 'Laravel'), /* |-------------------------------------------------------------------------- @@ -127,8 +127,6 @@ return [ 'enabled' => env('REGISTRATION_ENABLED', true), ], - 'pixel_logo_enabled' => env('PIXELLOGO_ENABLED', true), - 'force_https' => env('FORCE_HTTPS', false), 'puppeteer_docker' => env('PUPPETEER_DOCKER', false), 'puppeteer_mode' => env('PUPPETEER_MODE', 'local'), @@ -156,5 +154,5 @@ return [ 'catalog_url' => env('CATALOG_URL', 'https://raw.githubusercontent.com/bnussbau/trmnl-recipe-catalog/refs/heads/main/catalog.yaml'), - 'github_repo' => env('GITHUB_REPO', 'usetrmnl/larapaper'), + 'github_repo' => env('GITHUB_REPO', 'usetrmnl/byos_laravel'), ]; diff --git a/database/migrations/2026_02_27_153837_add_current_image_metadata_to_plugins_table.php b/database/migrations/2026_02_27_153837_add_current_image_metadata_to_plugins_table.php deleted file mode 100644 index d212fe7..0000000 --- a/database/migrations/2026_02_27_153837_add_current_image_metadata_to_plugins_table.php +++ /dev/null @@ -1,28 +0,0 @@ -json('current_image_metadata')->nullable()->after('current_image'); - }); - } - - /** - * Reverse the migrations. - */ - public function down(): void - { - Schema::table('plugins', function (Blueprint $table) { - $table->dropColumn('current_image_metadata'); - }); - } -}; diff --git a/database/seeders/DatabaseSeeder.php b/database/seeders/DatabaseSeeder.php index 5811f4c..c7125c5 100644 --- a/database/seeders/DatabaseSeeder.php +++ b/database/seeders/DatabaseSeeder.php @@ -3,7 +3,6 @@ namespace Database\Seeders; use App\Models\Device; -use App\Models\Playlist; use App\Models\Plugin; use App\Models\User; use Illuminate\Database\Seeder; @@ -24,19 +23,9 @@ class DatabaseSeeder extends Seeder 'password' => bcrypt('admin@example.com'), ]); - $device = Device::factory()->create([ + Device::factory(1)->create([ 'mac_address' => '00:00:00:00:00:00', 'api_key' => 'test-api-key', - 'proxy_cloud' => false, - ]); - - Playlist::factory()->create([ - 'device_id' => $device->id, - 'name' => 'Default', - 'is_active' => true, - 'active_from' => null, - 'active_until' => null, - 'weekdays' => null ]); // Device::factory(5)->create(); diff --git a/docker/prod/docker-compose.yml b/docker/prod/docker-compose.yml index eaa48fb..fb7dfc6 100644 --- a/docker/prod/docker-compose.yml +++ b/docker/prod/docker-compose.yml @@ -1,6 +1,6 @@ services: app: - image: ghcr.io/usetrmnl/larapaper:latest + image: ghcr.io/usetrmnl/byos_laravel:latest ports: - "4567:8080" environment: diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md index e2246bc..40bcbd3 100644 --- a/docs/DEVELOPMENT.md +++ b/docs/DEVELOPMENT.md @@ -9,7 +9,7 @@ #### Clone the repository ```bash -git clone git@github.com:usetrmnl/larapaper.git +git clone git@github.com:usetrmnl/byos_laravel.git ``` #### Copy environment file diff --git a/resources/views/components/app-logo.blade.php b/resources/views/components/app-logo.blade.php index f50634a..842020e 100644 --- a/resources/views/components/app-logo.blade.php +++ b/resources/views/components/app-logo.blade.php @@ -2,9 +2,5 @@
- @if(config('app.pixel_logo_enabled')) - - @else - LaraPaper - @endif + TRMNL BYOS Laravel
diff --git a/resources/views/components/auth-header.blade.php b/resources/views/components/auth-header.blade.php index 78aa2e5..e596a3f 100644 --- a/resources/views/components/auth-header.blade.php +++ b/resources/views/components/auth-header.blade.php @@ -4,10 +4,6 @@ ])
- @if(config('app.pixel_logo_enabled')) - - @else - LaraPaper - @endif + {{ $title }} {{ $description }}
diff --git a/resources/views/default-screens/setup.blade.php b/resources/views/default-screens/setup.blade.php index 9113eb6..ab7ec60 100644 --- a/resources/views/default-screens/setup.blade.php +++ b/resources/views/default-screens/setup.blade.php @@ -15,10 +15,10 @@ - Welcome to LaraPaper! + Welcome to BYOS Laravel! Your device is connected. - + diff --git a/resources/views/default-screens/sleep.blade.php b/resources/views/default-screens/sleep.blade.php index ef0a80c..fa0c8cd 100644 --- a/resources/views/default-screens/sleep.blade.php +++ b/resources/views/default-screens/sleep.blade.php @@ -25,6 +25,6 @@ Sleep Mode - + diff --git a/resources/views/livewire/plugins/index.blade.php b/resources/views/livewire/plugins/index.blade.php index 0985841..848fc67 100644 --- a/resources/views/livewire/plugins/index.blade.php +++ b/resources/views/livewire/plugins/index.blade.php @@ -264,7 +264,7 @@ new class extends Component {{--
  • date: "%N" is unsupported. Use date: "u" instead
  • --}} {{-- --}} - Please report issues on GitHub. Include your example zip file. + Please report issues on GitHub. Include your example zip file.
    @@ -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 392b0b9..150e626 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 - LaraPaper - “This screen was rendered by BYOS LaraPaper” + TRMNL BYOS Laravel + “This screen was rendered by BYOS Laravel” Benjamin Nussbaum diff --git a/resources/views/partials/head.blade.php b/resources/views/partials/head.blade.php index 26a42a6..fa0f31a 100644 --- a/resources/views/partials/head.blade.php +++ b/resources/views/partials/head.blade.php @@ -1,7 +1,7 @@ -{{ $title ?? 'LaraPaper' }} +{{ $title ?? 'TRMNL BYOS Laravel' }} diff --git a/resources/views/welcome.blade.php b/resources/views/welcome.blade.php index ba608f9..abf6a69 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/routes/api.php b/routes/api.php index 8fc040c..c759d61 100644 --- a/routes/api.php +++ b/routes/api.php @@ -88,8 +88,8 @@ Route::get('/display', function (Request $request) { $refreshTimeOverride = $playlistItem->playlist()->first()->refresh_time; $plugin = $playlistItem->plugin; - ImageGenerationService::resetIfNotCacheable($plugin, $device); - $plugin->refresh(); + // Reset cache if Devices with different dimensions exist + ImageGenerationService::resetIfNotCacheable($plugin); // Check and update stale data if needed if ($plugin->isDataStale() || $plugin->current_image === null) { @@ -699,9 +699,6 @@ Route::get('/display/{uuid}/alias', function (Request $request, string $uuid) { ], 404); } - ImageGenerationService::resetIfNotCacheable($plugin, $deviceModel); - $plugin->refresh(); - // Check if we can use cached image (only for og_png and if data is not stale) $useCache = $deviceModelName === 'og_png' && ! $plugin->isDataStale() && $plugin->current_image !== null; @@ -747,13 +744,9 @@ Route::get('/display/{uuid}/alias', function (Request $request, string $uuid) { palette: $deviceModel->palette ); - // Update plugin cache if using og_png (recipes only get metadata for cache comparison) + // Update plugin cache if using og_png if ($deviceModelName === 'og_png') { - $update = ['current_image' => $imageUuid]; - if ($plugin->plugin_type === 'recipe') { - $update['current_image_metadata'] = ImageGenerationService::buildImageMetadataFromDeviceModel($deviceModel); - } - $plugin->update($update); + $plugin->update(['current_image' => $imageUuid]); } // Return the generated image diff --git a/tests/Feature/GenerateScreenJobTest.php b/tests/Feature/GenerateScreenJobTest.php index 9482e8c..115fb51 100644 --- a/tests/Feature/GenerateScreenJobTest.php +++ b/tests/Feature/GenerateScreenJobTest.php @@ -2,8 +2,6 @@ use App\Jobs\GenerateScreenJob; use App\Models\Device; -use App\Models\DeviceModel; -use App\Models\Plugin; use Bnussbau\TrmnlPipeline\TrmnlPipeline; use Illuminate\Support\Facades\Storage; @@ -60,26 +58,3 @@ test('it preserves gitignore file during cleanup', function (): void { Storage::disk('public')->assertExists('/images/generated/.gitignore'); }); - -test('it saves current_image_metadata for recipe plugins', function (): void { - $deviceModel = DeviceModel::factory()->create([ - 'width' => 800, - 'height' => 480, - 'rotation' => 0, - 'mime_type' => 'image/png', - 'palette_id' => null, - ]); - $device = Device::factory()->create(['device_model_id' => $deviceModel->id]); - $plugin = Plugin::factory()->create(['plugin_type' => 'recipe']); - - $job = new GenerateScreenJob($device->id, $plugin->id, '
    Test
    '); - $job->handle(); - - $plugin->refresh(); - expect($plugin->current_image)->not->toBeNull(); - expect($plugin->current_image_metadata)->toBeArray(); - expect($plugin->current_image_metadata)->toHaveKeys(['width', 'height', 'rotation', 'palette_id', 'mime_type']); - expect($plugin->current_image_metadata['width'])->toBe(800); - expect($plugin->current_image_metadata['height'])->toBe(480); - expect($plugin->current_image_metadata['mime_type'])->toBe('image/png'); -}); diff --git a/tests/Feature/ImageGenerationServiceTest.php b/tests/Feature/ImageGenerationServiceTest.php index 9d26eb6..07bb6a6 100644 --- a/tests/Feature/ImageGenerationServiceTest.php +++ b/tests/Feature/ImageGenerationServiceTest.php @@ -8,7 +8,6 @@ use App\Models\DeviceModel; use App\Services\ImageGenerationService; use Bnussbau\TrmnlPipeline\TrmnlPipeline; use Illuminate\Foundation\Testing\RefreshDatabase; -use Illuminate\Support\Facades\Schema; use Illuminate\Support\Facades\Storage; uses(RefreshDatabase::class); @@ -23,10 +22,6 @@ afterEach(function (): void { TrmnlPipeline::restore(); }); -it('plugins table has current_image_metadata column', function (): void { - expect(Schema::hasColumn('plugins', 'current_image_metadata'))->toBeTrue(); -}); - it('generates image for device without device model', function (): void { // Create a device without a DeviceModel (legacy behavior) $device = Device::factory()->create([ @@ -275,15 +270,39 @@ it('cleanupFolder preserves .gitignore', function (): void { Storage::disk('public')->assertExists('/images/generated/.gitignore'); }); -it('resetIfNotCacheable does not reset recipe cache based on other devices', function (): void { - // Cache validity is now determined at use-time via current_image_metadata - $plugin = App\Models\Plugin::factory()->create(['current_image' => 'test-uuid', 'plugin_type' => 'recipe']); +it('resetIfNotCacheable resets when device models exist', function (): void { + // Create a plugin + $plugin = App\Models\Plugin::factory()->create(['current_image' => 'test-uuid']); - Device::factory()->create(['device_model_id' => DeviceModel::factory()->create()->id]); + // Create a device with DeviceModel (should trigger cache reset) + Device::factory()->create([ + 'device_model_id' => DeviceModel::factory()->create()->id, + ]); + + // Run reset check ImageGenerationService::resetIfNotCacheable($plugin); + // Assert plugin image was reset $plugin->refresh(); - expect($plugin->current_image)->toBe('test-uuid'); + expect($plugin->current_image)->toBeNull(); +}); + +it('resetIfNotCacheable resets when custom dimensions exist', function (): void { + // Create a plugin + $plugin = App\Models\Plugin::factory()->create(['current_image' => 'test-uuid']); + + // Create a device with custom dimensions (should trigger cache reset) + Device::factory()->create([ + 'width' => 1024, // Different from default 800 + 'height' => 768, // Different from default 480 + ]); + + // Run reset check + ImageGenerationService::resetIfNotCacheable($plugin); + + // Assert plugin image was reset + $plugin->refresh(); + expect($plugin->current_image)->toBeNull(); }); it('resetIfNotCacheable preserves image for standard devices', function (): void { @@ -306,122 +325,27 @@ it('resetIfNotCacheable preserves image for standard devices', function (): void }); it('cache is reset when plugin markup changes', function (): void { - // Create a plugin with cached image and metadata + // Create a plugin with cached image $plugin = App\Models\Plugin::factory()->create([ 'current_image' => 'cached-uuid', - 'current_image_metadata' => ['width' => 800, 'height' => 480, 'rotation' => 0, 'palette_id' => null, 'mime_type' => 'image/png'], 'render_markup' => '
    Original markup
    ', ]); - $plugin->update(['render_markup' => '
    Updated markup
    ']); + // Create devices with standard dimensions (cacheable) + Device::factory()->count(2)->create([ + 'width' => 800, + 'height' => 480, + 'rotate' => 0, + ]); + // Update the plugin markup + $plugin->update([ + 'render_markup' => '
    Updated markup
    ', + ]); + + // Assert cache was reset when markup changed $plugin->refresh(); expect($plugin->current_image)->toBeNull(); - expect($plugin->current_image_metadata)->toBeNull(); -}); - -it('buildImageMetadataFromDevice returns canonical metadata shape', function (): void { - $deviceModel = DeviceModel::factory()->create([ - 'width' => 800, - 'height' => 480, - 'rotation' => 0, - 'mime_type' => 'image/png', - 'palette_id' => null, - ]); - $device = Device::factory()->create(['device_model_id' => $deviceModel->id]); - - $meta = ImageGenerationService::buildImageMetadataFromDevice($device); - - expect($meta)->toHaveKeys(['width', 'height', 'rotation', 'palette_id', 'mime_type']); - expect($meta['width'])->toBe(800); - expect($meta['height'])->toBe(480); - expect($meta['rotation'])->toBe(0); - expect($meta['mime_type'])->toBe('image/png'); -}); - -it('buildImageMetadataFromDeviceModel returns canonical metadata shape', function (): void { - $model = DeviceModel::factory()->create([ - 'width' => 1024, - 'height' => 768, - 'rotation' => 90, - 'mime_type' => 'image/bmp', - 'palette_id' => null, - ]); - - $meta = ImageGenerationService::buildImageMetadataFromDeviceModel($model); - - expect($meta)->toHaveKeys(['width', 'height', 'rotation', 'palette_id', 'mime_type']); - expect($meta['width'])->toBe(1024); - expect($meta['height'])->toBe(768); - expect($meta['rotation'])->toBe(90); - expect($meta['mime_type'])->toBe('image/bmp'); -}); - -it('imageMetadataMatches returns false when stored is null or empty', function (): void { - $device = Device::factory()->create(['width' => 800, 'height' => 480, 'rotate' => 0]); - - expect(ImageGenerationService::imageMetadataMatches(null, $device))->toBeFalse(); - expect(ImageGenerationService::imageMetadataMatches([], $device))->toBeFalse(); -}); - -it('imageMetadataMatches returns true when metadata matches device', function (): void { - $deviceModel = DeviceModel::factory()->create([ - 'width' => 800, - 'height' => 480, - 'rotation' => 0, - 'mime_type' => 'image/png', - 'palette_id' => null, - ]); - $device = Device::factory()->create(['device_model_id' => $deviceModel->id]); - $stored = ImageGenerationService::buildImageMetadataFromDevice($device); - - expect(ImageGenerationService::imageMetadataMatches($stored, $device))->toBeTrue(); -}); - -it('imageMetadataMatches returns false when metadata differs', function (): void { - $device = Device::factory()->create(['width' => 800, 'height' => 480, 'rotate' => 0]); - $stored = ['width' => 800, 'height' => 480, 'rotation' => 0, 'palette_id' => null, 'mime_type' => 'image/png']; - - $device->update(['width' => 1024]); - $device->refresh(); - - expect(ImageGenerationService::imageMetadataMatches($stored, $device))->toBeFalse(); -}); - -it('resetIfNotCacheable clears recipe cache when metadata does not match', function (): void { - $plugin = App\Models\Plugin::factory()->create([ - 'plugin_type' => 'recipe', - 'current_image' => 'cached-uuid', - 'current_image_metadata' => ['width' => 800, 'height' => 480, 'rotation' => 0, 'palette_id' => null, 'mime_type' => 'image/png'], - ]); - $device = Device::factory()->create(['width' => 1024, 'height' => 768, 'rotate' => 0]); - - ImageGenerationService::resetIfNotCacheable($plugin, $device); - - $plugin->refresh(); - expect($plugin->current_image)->toBeNull(); - expect($plugin->current_image_metadata)->toBeNull(); -}); - -it('resetIfNotCacheable preserves cache when metadata matches', function (): void { - $deviceModel = DeviceModel::factory()->create([ - 'width' => 800, - 'height' => 480, - 'rotation' => 0, - 'mime_type' => 'image/png', - ]); - $device = Device::factory()->create(['device_model_id' => $deviceModel->id]); - $meta = ImageGenerationService::buildImageMetadataFromDevice($device); - $plugin = App\Models\Plugin::factory()->create([ - 'plugin_type' => 'recipe', - 'current_image' => 'cached-uuid', - 'current_image_metadata' => $meta, - ]); - - ImageGenerationService::resetIfNotCacheable($plugin, $device); - - $plugin->refresh(); - expect($plugin->current_image)->toBe('cached-uuid'); }); it('determines correct image format from device model', function (): void { diff --git a/tests/Feature/PixelLogoConfigTest.php b/tests/Feature/PixelLogoConfigTest.php deleted file mode 100644 index ba009c9..0000000 --- a/tests/Feature/PixelLogoConfigTest.php +++ /dev/null @@ -1,46 +0,0 @@ -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); -}); diff --git a/tests/Unit/Services/ImageGenerationServiceTest.php b/tests/Unit/Services/ImageGenerationServiceTest.php index da9bef9..5e3dc47 100644 --- a/tests/Unit/Services/ImageGenerationServiceTest.php +++ b/tests/Unit/Services/ImageGenerationServiceTest.php @@ -176,15 +176,37 @@ it('cleanup_folder identifies active images correctly', function (): void { expect($activeImageUuids)->not->toContain(null); }); -it('reset_if_not_cacheable does not reset recipe cache when other devices exist', function (): void { - // Cache validity is now determined at use-time via metadata - $plugin = App\Models\Plugin::factory()->create(['current_image' => 'test-uuid', 'plugin_type' => 'recipe']); - Device::factory()->create(['device_model_id' => DeviceModel::factory()->create()->id]); +it('reset_if_not_cacheable detects device models', function (): void { + // Create a plugin + $plugin = App\Models\Plugin::factory()->create(['current_image' => 'test-uuid']); + // Create a device with DeviceModel + Device::factory()->create([ + 'device_model_id' => DeviceModel::factory()->create()->id, + ]); + + // Test that the method detects DeviceModels and resets cache ImageGenerationService::resetIfNotCacheable($plugin); $plugin->refresh(); - expect($plugin->current_image)->toBe('test-uuid'); + expect($plugin->current_image)->toBeNull(); +}); + +it('reset_if_not_cacheable detects custom dimensions', function (): void { + // Create a plugin + $plugin = App\Models\Plugin::factory()->create(['current_image' => 'test-uuid']); + + // Create a device with custom dimensions + Device::factory()->create([ + 'width' => 1024, // Different from default 800 + 'height' => 768, // Different from default 480 + ]); + + // Test that the method detects custom dimensions and resets cache + ImageGenerationService::resetIfNotCacheable($plugin); + + $plugin->refresh(); + expect($plugin->current_image)->toBeNull(); }); it('reset_if_not_cacheable preserves cache for standard devices', function (): void { @@ -236,21 +258,26 @@ it('reset_if_not_cacheable preserves cache for og_png and og_plus device models' expect($plugin->current_image)->toBe('test-uuid'); }); -it('reset_if_not_cacheable does not reset cache for non-standard device models', function (): void { - // Cache is now validated at use-time via metadata comparison - $plugin = App\Models\Plugin::factory()->create(['current_image' => 'test-uuid', 'plugin_type' => 'recipe']); +it('reset_if_not_cacheable resets cache for non-standard device models', function (): void { + // Create a plugin + $plugin = App\Models\Plugin::factory()->create(['current_image' => 'test-uuid']); + + // Create a non-standard device model (e.g., kindle) $kindleModel = DeviceModel::factory()->create([ 'name' => 'test_amazon_kindle_2024', 'width' => 1400, 'height' => 840, 'rotation' => 90, ]); + + // Create a device with the non-standard device model Device::factory()->create(['device_model_id' => $kindleModel->id]); + // Test that the method resets cache for non-standard device models ImageGenerationService::resetIfNotCacheable($plugin); $plugin->refresh(); - expect($plugin->current_image)->toBe('test-uuid'); + expect($plugin->current_image)->toBeNull(); }); it('reset_if_not_cacheable handles null plugin', function (): void {