diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..46262d8 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,51 @@ +name: tests + +on: + push: + branches: + - develop + - main + pull_request: + branches: + - develop + - main + +jobs: + ci: + runs-on: ubuntu-latest + environment: Testing + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: 8.4 + tools: composer:v2 + coverage: xdebug + + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: '22' + cache: 'npm' + + - name: Install Node Dependencies + run: npm i + + - name: Install Dependencies + run: composer install --no-interaction --prefer-dist --optimize-autoloader + + - name: Copy Environment File + run: cp .env.example .env + + - name: Generate Application Key + run: php artisan key:generate + + - name: Build Assets + run: npm run build + + - name: Run Tests + run: ./vendor/bin/pest diff --git a/README.md b/README.md index 5ecae25..2445491 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,24 @@ ## Laravel Trmnl Server -This is a PoC of a TRMNL server, written in Laravel. Inspired by https://github.com/usetrmnl/byos_sinatra +Laravel Trmnl Server is a self-hostable implementation of a TRMNL server, built with Laravel. +It enables you to manage TRMNL devices, generate screens dynamically, and can act as a proxy for the TRMNL API (native plugin system). +Inspired by [usetrmnl/byos_sinatra](https://github.com/usetrmnl/byos_sinatra). ![Screenshot](README_byos-screenshot.png) +[More Screenshots](screenshots/SCREENSHOTS.md) + +### Key Features + +* ๐Ÿ“ก Device Information โ€“ Display battery status, WiFi strength, firmware version, and more. +* ๐Ÿ” Auto-Join โ€“ Automatically detects and adds devices from your local network. +* ๐Ÿ–ฅ๏ธ Screen Generation โ€“ Supports Markup, API, or update via Code. +* ๐Ÿ”„ TRMNL API Proxy โ€“ Can act as a proxy for TRMNL API (requires TRMNL Developer Edition). + * This enables a hybrid setup โ€“ for example, you can update your custom Train Monitor every 5 minutes in the morning, while displaying native TRMNL plugins throughout the day. +* ๐Ÿณ Deployment โ€“ Dockerized setup for easier hosting (Dockerfile, docker-compose). + ### Requirements + * PHP >= 8.2 * ext-imagick * puppeteer [see Browsershot docs](https://spatie.be/docs/browsershot/v4/requirements) @@ -45,31 +59,190 @@ To make your server accessible in the network, you can run the following command php artisan serve --host=0.0.0.0 --port 4567 ``` +### Docker +Use the provided Dockerfile, or docker-compose file to run the server in a container. +You can persist the database file by mounting a volume to `/var/www/html/database/database.sqlite`. + +```Dockerfile +# docker-compose.yaml +volumes: + - ./database/database.sqlite:/var/www/html/database/database.sqlite +``` + ### Usage +#### Environment Variables + +| environment | description | default | +|-------------------------------|------------------------------------------------------------------|-------------------| +| `TRMNL_PROXY_BASE_URL` | Base URL of the native TRMNL service | https://trmnl.app | +| `TRMNL_PROXY_REFRESH_MINUTES` | How often should the server fetch new images from native service | 15 | +| `REGISTRATION_ENABLED` | Allow user registration via Webinterface | 1 | + +#### Login + If your environment is local, you can access the server at `http://localhost:4567` and login with user / password -admin@example.com / admin@example.com +`admin@example.com` / `admin@example.com`, otherwise register. With environment variable `REGISTRATION_ENABLED` you can control, if registration is allowed. -#### Add your TRMNL Device +#### โž• Add Your TRMNL Device -http://localhost:4567/devices +##### Auto-Join (Local Network) -* Add new device -* You can grab the TRMNL Mac Address and API Key from the TRMNL Dashboard. Or debug the incoming request to `/api/setup` to determine. +1. Switch on the โ€œPermit Auto-Joinโ€ toggle in the header. For that to work only one user can be registered. +2. New devices on your local network will be detected and added automatically when connecting to the server. + +โœ… This is the easiest way to connect your devices with minimal effort. + +##### Manually + +1. Open the Devices page: +๐Ÿ‘‰ http://localhost:4567/devices +2. Click โ€œAdd New Deviceโ€. +3. Retrieve your TRMNL MAC Address and API Key: + - You can grab the TRMNL Mac Address and API Key from the TRMNL Dashboard + - Alternatively, debug incoming requests to /api/setup to determine them + +### โš™๏ธ Configure Server for Device + +#### ๐Ÿ“Œ Firmware Version 1.4.6 or Newer + +* Setup device +* After entering Wifi credentials, choose "Custom Server" +* Point to the URL of your server + +#### Firmware Older Than 1.4.6 + +If your device firmware is older than 1.4.6, you need to flash a new firmware version to point it to your server. -#### Flash Firmware to point Device to your server See this YouTube guide: [https://www.youtube.com/watch?v=3xehPW-PCOM](https://www.youtube.com/watch?v=3xehPW-PCOM) -#### Generate Screen +### ๐Ÿ–ฅ๏ธ Generate Screens -* Edit resources/views/trmnl.blade.php +#### ๐ŸŽจ Blade View +* Edit `resources/views/trmnl.blade.php` + * Available Blade Components are listed here: [laravel-trmnl | Blade Components](https://github.com/bnussbau/laravel-trmnl/tree/main/resources/views/components) * To generate the screen, run ```bash php artisan trmnl:screen:generate ``` +#### Generate via API +You can dynamically update screens by sending a POST request. +* Send a POST request to `/api/screen` with the following payload + +##### Header + +`Authorization` `Bearer ` + +##### Body + +```json +{ + "markup": "

Hello World

" +} +``` + +Token can be retrieved under Plugins > API in the Web Interface. + +#### Markup via Web Interface + +1. Navigate to Plugins > Markup in the Web Interface. +2. Enter your markup manually or select from the available templates. +3. Save and apply the changes. + +* Available Blade Components are listed here: [laravel-trmnl | Blade Components](https://github.com/bnussbau/laravel-trmnl/tree/main/resources/views/components) + +#### ๐Ÿ› ๏ธ Generate Screens Programmatically + +You can fetch external data, process it, and generate screens dynamically. +* Fetch data from an external source. +* Either render it in a Blade view or directly insert markup. +* Use Laravelโ€™s scheduler to automate updates. + +#### ๐Ÿ“Œ Example: Fetch Train Monitor Data + +This example retrieves data from [trmnl-train-monitor](https://github.com/bnussbau/trmnl-train-monitor) and updates the screen periodically. + +##### Step 1: Create a new Artisan Command + +```bash +php artisan make:command PluginTrainMonitorFetch +``` + +##### Step 2: Edit PluginTrainMonitorFetch.php + +```php +class PluginTrainMonitorFetch extends Command +{ + protected $signature = 'plugin:train:fetch'; + + protected $description = 'Command description'; + + public function handle(): void + { + $markup = Http::get('https://oebb.trmnl.yourserver.at/markup')->json('markup'); + GenerateScreenJob::dispatchSync(1, $markup); + } +} +``` + +##### Step 3: Schedule the Command in console.php + +```php +Schedule::command('plugin:train:fetch') + ->everyFiveMinutes() + ->timezone('Europe/Vienna') + ->between('5:00', '18:00'); +``` + +This will automatically update the screen every 5 minutes between 5:00 AM and 6:00 PM local time. + +### ๐Ÿค Contribution +Contributions are welcome! If youโ€™d like to improve the project, follow these steps: + +1. Open an Issue + - Before submitting a pull request, create an issue to discuss your idea. + - Clearly describe the feature or bug fix you want to work on. +2. Fork the Repository & Create a Branch +3. Make Your Changes & Add Tests + - Ensure your code follows best practices. + - Add Pest tests to cover your changes. +4. Run Tests + - `php artisan test` +5. Submit a Pull Request (PR) + - Push your branch and create a PR. + - Provide a clear description of your changes. + +๐Ÿš€ Thank you for contributing! Every contribution helps improve the project. + +### ๐Ÿ—๏ธ Roadmap + +Here are some features and improvements that are open for contribution: + +##### ๐Ÿ”Œ Plugin System + +- Enable configurable plugins via the Web Interface. +- Ensure compatibility with the trmnl-laravel package. +- Implement auto-discovery for plugins. + +##### โณ Scheduling + +- Move task scheduling from console.php to a Web Interface. +- Allow users to configure custom schedule intervals. + +##### ๐Ÿ–ฅ๏ธ โ€œNativeโ€ Plugins +- Add built-in plugins such as (as an example): + - โ˜๏ธ Weather + - ๐Ÿ’ฌ Quotes + - ๐Ÿก Home Assistant integration +- Provide Web UI controls to enable/disable plugins. + +##### Improve Code Coverage + +- Expand Pest tests to cover more functionality. +- Increase code coverage (currently at 86.9%). ### License MIT diff --git a/README_byos-screenshot.png b/README_byos-screenshot.png index 1e99bfb..7cd524a 100644 Binary files a/README_byos-screenshot.png and b/README_byos-screenshot.png differ diff --git a/composer.json b/composer.json index 5b445d1..e5d4a74 100644 --- a/composer.json +++ b/composer.json @@ -19,7 +19,8 @@ "laravel/tinker": "^2.10.1", "livewire/flux": "^2.0", "livewire/volt": "^1.6.7", - "spatie/browsershot": "^5.0" + "spatie/browsershot": "^5.0", + "spatie/pest-expectations": "^1.3" }, "require-dev": { "fakerphp/faker": "^1.23", diff --git a/composer.lock b/composer.lock index c853906..02acf1d 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": "2820695c16f3a51b2464c1ed739338ba", + "content-hash": "a37113926b52744df508a5619719b4dc", "packages": [ { "name": "bnussbau/laravel-trmnl", @@ -87,16 +87,16 @@ }, { "name": "brick/math", - "version": "0.12.2", + "version": "0.12.3", "source": { "type": "git", "url": "https://github.com/brick/math.git", - "reference": "901eddb1e45a8e0f689302e40af871c181ecbe40" + "reference": "866551da34e9a618e64a819ee1e01c20d8a588ba" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/brick/math/zipball/901eddb1e45a8e0f689302e40af871c181ecbe40", - "reference": "901eddb1e45a8e0f689302e40af871c181ecbe40", + "url": "https://api.github.com/repos/brick/math/zipball/866551da34e9a618e64a819ee1e01c20d8a588ba", + "reference": "866551da34e9a618e64a819ee1e01c20d8a588ba", "shasum": "" }, "require": { @@ -135,7 +135,7 @@ ], "support": { "issues": "https://github.com/brick/math/issues", - "source": "https://github.com/brick/math/tree/0.12.2" + "source": "https://github.com/brick/math/tree/0.12.3" }, "funding": [ { @@ -143,7 +143,7 @@ "type": "github" } ], - "time": "2025-02-26T10:21:45+00:00" + "time": "2025-02-28T13:11:00+00:00" }, { "name": "carbonphp/carbon-doctrine-types", @@ -1203,16 +1203,16 @@ }, { "name": "intervention/image", - "version": "3.11.1", + "version": "3.11.2", "source": { "type": "git", "url": "https://github.com/Intervention/image.git", - "reference": "0f87254688e480fbb521e2a1ac6c11c784ca41af" + "reference": "ebbb711871fb261c064cf4c422f5f3c124fe1842" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Intervention/image/zipball/0f87254688e480fbb521e2a1ac6c11c784ca41af", - "reference": "0f87254688e480fbb521e2a1ac6c11c784ca41af", + "url": "https://api.github.com/repos/Intervention/image/zipball/ebbb711871fb261c064cf4c422f5f3c124fe1842", + "reference": "ebbb711871fb261c064cf4c422f5f3c124fe1842", "shasum": "" }, "require": { @@ -1223,7 +1223,7 @@ "require-dev": { "mockery/mockery": "^1.6", "phpstan/phpstan": "^2.1", - "phpunit/phpunit": "^10.0 || ^11.0", + "phpunit/phpunit": "^10.0 || ^11.0 || ^12.0", "slevomat/coding-standard": "~8.0", "squizlabs/php_codesniffer": "^3.8" }, @@ -1259,7 +1259,7 @@ ], "support": { "issues": "https://github.com/Intervention/image/issues", - "source": "https://github.com/Intervention/image/tree/3.11.1" + "source": "https://github.com/Intervention/image/tree/3.11.2" }, "funding": [ { @@ -1275,7 +1275,7 @@ "type": "ko_fi" } ], - "time": "2025-02-01T07:28:26+00:00" + "time": "2025-02-27T13:08:55+00:00" }, { "name": "laravel/framework", @@ -2295,16 +2295,16 @@ }, { "name": "livewire/flux", - "version": "v2.0.3", + "version": "v2.0.4", "source": { "type": "git", "url": "https://github.com/livewire/flux.git", - "reference": "dec010f09419cd9d9930abc4b304802c379be57e" + "reference": "6b0d59040715f072982bfc92fe71414b44d45a0c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/livewire/flux/zipball/dec010f09419cd9d9930abc4b304802c379be57e", - "reference": "dec010f09419cd9d9930abc4b304802c379be57e", + "url": "https://api.github.com/repos/livewire/flux/zipball/6b0d59040715f072982bfc92fe71414b44d45a0c", + "reference": "6b0d59040715f072982bfc92fe71414b44d45a0c", "shasum": "" }, "require": { @@ -2352,9 +2352,9 @@ ], "support": { "issues": "https://github.com/livewire/flux/issues", - "source": "https://github.com/livewire/flux/tree/v2.0.3" + "source": "https://github.com/livewire/flux/tree/v2.0.4" }, - "time": "2025-02-26T00:29:58+00:00" + "time": "2025-02-28T16:35:28+00:00" }, { "name": "livewire/livewire", @@ -3618,16 +3618,16 @@ }, { "name": "ramsey/collection", - "version": "2.0.0", + "version": "2.1.0", "source": { "type": "git", "url": "https://github.com/ramsey/collection.git", - "reference": "a4b48764bfbb8f3a6a4d1aeb1a35bb5e9ecac4a5" + "reference": "3c5990b8a5e0b79cd1cf11c2dc1229e58e93f109" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/ramsey/collection/zipball/a4b48764bfbb8f3a6a4d1aeb1a35bb5e9ecac4a5", - "reference": "a4b48764bfbb8f3a6a4d1aeb1a35bb5e9ecac4a5", + "url": "https://api.github.com/repos/ramsey/collection/zipball/3c5990b8a5e0b79cd1cf11c2dc1229e58e93f109", + "reference": "3c5990b8a5e0b79cd1cf11c2dc1229e58e93f109", "shasum": "" }, "require": { @@ -3635,25 +3635,22 @@ }, "require-dev": { "captainhook/plugin-composer": "^5.3", - "ergebnis/composer-normalize": "^2.28.3", - "fakerphp/faker": "^1.21", + "ergebnis/composer-normalize": "^2.45", + "fakerphp/faker": "^1.24", "hamcrest/hamcrest-php": "^2.0", - "jangregor/phpstan-prophecy": "^1.0", - "mockery/mockery": "^1.5", + "jangregor/phpstan-prophecy": "^2.1", + "mockery/mockery": "^1.6", "php-parallel-lint/php-console-highlighter": "^1.0", - "php-parallel-lint/php-parallel-lint": "^1.3", - "phpcsstandards/phpcsutils": "^1.0.0-rc1", - "phpspec/prophecy-phpunit": "^2.0", - "phpstan/extension-installer": "^1.2", - "phpstan/phpstan": "^1.9", - "phpstan/phpstan-mockery": "^1.1", - "phpstan/phpstan-phpunit": "^1.3", - "phpunit/phpunit": "^9.5", - "psalm/plugin-mockery": "^1.1", - "psalm/plugin-phpunit": "^0.18.4", - "ramsey/coding-standard": "^2.0.3", - "ramsey/conventional-commits": "^1.3", - "vimeo/psalm": "^5.4" + "php-parallel-lint/php-parallel-lint": "^1.4", + "phpspec/prophecy-phpunit": "^2.3", + "phpstan/extension-installer": "^1.4", + "phpstan/phpstan": "^2.1", + "phpstan/phpstan-mockery": "^2.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpunit/phpunit": "^10.5", + "ramsey/coding-standard": "^2.3", + "ramsey/conventional-commits": "^1.6", + "roave/security-advisories": "dev-latest" }, "type": "library", "extra": { @@ -3691,19 +3688,9 @@ ], "support": { "issues": "https://github.com/ramsey/collection/issues", - "source": "https://github.com/ramsey/collection/tree/2.0.0" + "source": "https://github.com/ramsey/collection/tree/2.1.0" }, - "funding": [ - { - "url": "https://github.com/ramsey", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/ramsey/collection", - "type": "tidelift" - } - ], - "time": "2022-12-31T21:50:55+00:00" + "time": "2025-03-02T04:48:29+00:00" }, { "name": "ramsey/uuid", @@ -3925,6 +3912,68 @@ ], "time": "2025-02-06T14:58:20+00:00" }, + { + "name": "spatie/pest-expectations", + "version": "1.3.0", + "source": { + "type": "git", + "url": "https://github.com/spatie/pest-expectations.git", + "reference": "e7e7be733f315157da97a44988099374edeffc23" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/spatie/pest-expectations/zipball/e7e7be733f315157da97a44988099374edeffc23", + "reference": "e7e7be733f315157da97a44988099374edeffc23", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "require-dev": { + "illuminate/contracts": "^9.47|^10.0", + "laravel/pint": "^1.2", + "pestphp/pest": "^1.20|^2.0", + "spatie/ray": "^1.28" + }, + "type": "library", + "autoload": { + "files": [ + "src/PestExpectations.php", + "src/Helpers.php" + ], + "psr-4": { + "Spatie\\PestExpectations\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Freek Van der Herten", + "email": "freek@spatie.be", + "role": "Developer" + } + ], + "description": "A collection of handy custom Pest customisations", + "homepage": "https://github.com/spatie/pest-expectations", + "keywords": [ + "pest-expectations", + "spatie" + ], + "support": { + "issues": "https://github.com/spatie/pest-expectations/issues", + "source": "https://github.com/spatie/pest-expectations/tree/1.3.0" + }, + "funding": [ + { + "url": "https://github.com/spatie", + "type": "github" + } + ], + "time": "2023-04-05T17:19:05+00:00" + }, { "name": "spatie/temporary-directory", "version": "2.3.0", diff --git a/config/services.php b/config/services.php index 7e05c6b..1706b99 100644 --- a/config/services.php +++ b/config/services.php @@ -38,6 +38,7 @@ return [ 'trmnl' => [ 'proxy_base_url' => env('TRMNL_PROXY_BASE_URL', 'https://trmnl.app'), 'proxy_refresh_minutes' => env('TRMNL_PROXY_REFRESH_MINUTES', 15), + 'override_orig_icon' => env('TRMNL_OVERRIDE_ORIG_ICON', false), ], ]; diff --git a/database/factories/DeviceFactory.php b/database/factories/DeviceFactory.php index c4db337..eebf0a9 100644 --- a/database/factories/DeviceFactory.php +++ b/database/factories/DeviceFactory.php @@ -14,12 +14,17 @@ class DeviceFactory extends Factory public function definition(): array { return [ - 'name' => $this->faker->firstName().' TRMNL', + 'name' => $this->faker->firstName().'\'s TRMNL', 'mac_address' => $this->faker->macAddress(), 'default_refresh_interval' => '900', 'friendly_id' => Str::random(6), 'api_key' => 'tD-'.Str::random(19), 'user_id' => 1, + 'last_battery_voltage' => $this->faker->randomFloat(2, 3.0, 4.2), + 'last_rssi_level' => $this->faker->numberBetween(-100, 0), + 'last_firmware_version' => '1.6.0', + 'proxy_cloud' => $this->faker->boolean(), + 'last_log_request' => ['status' => 'success', 'timestamp' => Carbon::now()->toDateTimeString()], 'created_at' => Carbon::now(), 'updated_at' => Carbon::now(), ]; diff --git a/database/factories/UserFactory.php b/database/factories/UserFactory.php index 584104c..c5d3f2c 100644 --- a/database/factories/UserFactory.php +++ b/database/factories/UserFactory.php @@ -29,6 +29,7 @@ class UserFactory extends Factory 'email_verified_at' => now(), 'password' => static::$password ??= Hash::make('password'), 'remember_token' => Str::random(10), + 'assign_new_devices' => false, ]; } diff --git a/database/seeders/DatabaseSeeder.php b/database/seeders/DatabaseSeeder.php index 75f3991..1ab9ab8 100644 --- a/database/seeders/DatabaseSeeder.php +++ b/database/seeders/DatabaseSeeder.php @@ -2,10 +2,12 @@ namespace Database\Seeders; +use App\Models\Device; use App\Models\User; -// use Illuminate\Database\Console\Seeds\WithoutModelEvents; use Illuminate\Database\Seeder; +// use Illuminate\Database\Console\Seeds\WithoutModelEvents; + class DatabaseSeeder extends Seeder { /** @@ -13,12 +15,14 @@ class DatabaseSeeder extends Seeder */ public function run(): void { - // User::factory(10)->create(); + if (app()->isLocal()) { + User::factory()->create([ + 'name' => 'Test User', + 'email' => 'admin@example.com', + 'password' => bcrypt('admin@example.com'), + ]); - User::factory()->create([ - 'name' => 'Test User', - 'email' => 'admin@example.com', - 'password' => bcrypt('admin@example.com'), - ]); + // Device::factory(5)->create(); + } } } diff --git a/package-lock.json b/package-lock.json index c7726f3..5b1bbf9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -465,9 +465,9 @@ } }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.34.8", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.34.8.tgz", - "integrity": "sha512-q217OSE8DTp8AFHuNHXo0Y86e1wtlfVrXiAlwkIvGRQv9zbc6mE3sjIVfwI8sYUyNxwOg0j/Vm1RKM04JcWLJw==", + "version": "4.34.9", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.34.9.tgz", + "integrity": "sha512-qZdlImWXur0CFakn2BJ2znJOdqYZKiedEPEVNTBrpfPjc/YuTGcaYZcdmNFTkUj3DU0ZM/AElcM8Ybww3xVLzA==", "cpu": [ "arm" ], @@ -478,9 +478,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.34.8", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.34.8.tgz", - "integrity": "sha512-Gigjz7mNWaOL9wCggvoK3jEIUUbGul656opstjaUSGC3eT0BM7PofdAJaBfPFWWkXNVAXbaQtC99OCg4sJv70Q==", + "version": "4.34.9", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.34.9.tgz", + "integrity": "sha512-4KW7P53h6HtJf5Y608T1ISKvNIYLWRKMvfnG0c44M6In4DQVU58HZFEVhWINDZKp7FZps98G3gxwC1sb0wXUUg==", "cpu": [ "arm64" ], @@ -491,9 +491,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.34.8", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.34.8.tgz", - "integrity": "sha512-02rVdZ5tgdUNRxIUrFdcMBZQoaPMrxtwSb+/hOfBdqkatYHR3lZ2A2EGyHq2sGOd0Owk80oV3snlDASC24He3Q==", + "version": "4.34.9", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.34.9.tgz", + "integrity": "sha512-0CY3/K54slrzLDjOA7TOjN1NuLKERBgk9nY5V34mhmuu673YNb+7ghaDUs6N0ujXR7fz5XaS5Aa6d2TNxZd0OQ==", "cpu": [ "arm64" ], @@ -504,9 +504,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.34.8", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.34.8.tgz", - "integrity": "sha512-qIP/elwR/tq/dYRx3lgwK31jkZvMiD6qUtOycLhTzCvrjbZ3LjQnEM9rNhSGpbLXVJYQ3rq39A6Re0h9tU2ynw==", + "version": "4.34.9", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.34.9.tgz", + "integrity": "sha512-eOojSEAi/acnsJVYRxnMkPFqcxSMFfrw7r2iD9Q32SGkb/Q9FpUY1UlAu1DH9T7j++gZ0lHjnm4OyH2vCI7l7Q==", "cpu": [ "x64" ], @@ -517,9 +517,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.34.8", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.34.8.tgz", - "integrity": "sha512-IQNVXL9iY6NniYbTaOKdrlVP3XIqazBgJOVkddzJlqnCpRi/yAeSOa8PLcECFSQochzqApIOE1GHNu3pCz+BDA==", + "version": "4.34.9", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.34.9.tgz", + "integrity": "sha512-2lzjQPJbN5UnHm7bHIUKFMulGTQwdvOkouJDpPysJS+QFBGDJqcfh+CxxtG23Ik/9tEvnebQiylYoazFMAgrYw==", "cpu": [ "arm64" ], @@ -530,9 +530,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.34.8", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.34.8.tgz", - "integrity": "sha512-TYXcHghgnCqYFiE3FT5QwXtOZqDj5GmaFNTNt3jNC+vh22dc/ukG2cG+pi75QO4kACohZzidsq7yKTKwq/Jq7Q==", + "version": "4.34.9", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.34.9.tgz", + "integrity": "sha512-SLl0hi2Ah2H7xQYd6Qaiu01kFPzQ+hqvdYSoOtHYg/zCIFs6t8sV95kaoqjzjFwuYQLtOI0RZre/Ke0nPaQV+g==", "cpu": [ "x64" ], @@ -543,9 +543,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.34.8", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.34.8.tgz", - "integrity": "sha512-A4iphFGNkWRd+5m3VIGuqHnG3MVnqKe7Al57u9mwgbyZ2/xF9Jio72MaY7xxh+Y87VAHmGQr73qoKL9HPbXj1g==", + "version": "4.34.9", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.34.9.tgz", + "integrity": "sha512-88I+D3TeKItrw+Y/2ud4Tw0+3CxQ2kLgu3QvrogZ0OfkmX/DEppehus7L3TS2Q4lpB+hYyxhkQiYPJ6Mf5/dPg==", "cpu": [ "arm" ], @@ -556,9 +556,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.34.8", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.34.8.tgz", - "integrity": "sha512-S0lqKLfTm5u+QTxlFiAnb2J/2dgQqRy/XvziPtDd1rKZFXHTyYLoVL58M/XFwDI01AQCDIevGLbQrMAtdyanpA==", + "version": "4.34.9", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.34.9.tgz", + "integrity": "sha512-3qyfWljSFHi9zH0KgtEPG4cBXHDFhwD8kwg6xLfHQ0IWuH9crp005GfoUUh/6w9/FWGBwEHg3lxK1iHRN1MFlA==", "cpu": [ "arm" ], @@ -569,9 +569,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.34.8", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.34.8.tgz", - "integrity": "sha512-jpz9YOuPiSkL4G4pqKrus0pn9aYwpImGkosRKwNi+sJSkz+WU3anZe6hi73StLOQdfXYXC7hUfsQlTnjMd3s1A==", + "version": "4.34.9", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.34.9.tgz", + "integrity": "sha512-6TZjPHjKZUQKmVKMUowF3ewHxctrRR09eYyvT5eFv8w/fXarEra83A2mHTVJLA5xU91aCNOUnM+DWFMSbQ0Nxw==", "cpu": [ "arm64" ], @@ -582,9 +582,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.34.8", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.34.8.tgz", - "integrity": "sha512-KdSfaROOUJXgTVxJNAZ3KwkRc5nggDk+06P6lgi1HLv1hskgvxHUKZ4xtwHkVYJ1Rep4GNo+uEfycCRRxht7+Q==", + "version": "4.34.9", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.34.9.tgz", + "integrity": "sha512-LD2fytxZJZ6xzOKnMbIpgzFOuIKlxVOpiMAXawsAZ2mHBPEYOnLRK5TTEsID6z4eM23DuO88X0Tq1mErHMVq0A==", "cpu": [ "arm64" ], @@ -595,9 +595,9 @@ ] }, "node_modules/@rollup/rollup-linux-loongarch64-gnu": { - "version": "4.34.8", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.34.8.tgz", - "integrity": "sha512-NyF4gcxwkMFRjgXBM6g2lkT58OWztZvw5KkV2K0qqSnUEqCVcqdh2jN4gQrTn/YUpAcNKyFHfoOZEer9nwo6uQ==", + "version": "4.34.9", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.34.9.tgz", + "integrity": "sha512-dRAgTfDsn0TE0HI6cmo13hemKpVHOEyeciGtvlBTkpx/F65kTvShtY/EVyZEIfxFkV5JJTuQ9tP5HGBS0hfxIg==", "cpu": [ "loong64" ], @@ -608,9 +608,9 @@ ] }, "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.34.8", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.34.8.tgz", - "integrity": "sha512-LMJc999GkhGvktHU85zNTDImZVUCJ1z/MbAJTnviiWmmjyckP5aQsHtcujMjpNdMZPT2rQEDBlJfubhs3jsMfw==", + "version": "4.34.9", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.34.9.tgz", + "integrity": "sha512-PHcNOAEhkoMSQtMf+rJofwisZqaU8iQ8EaSps58f5HYll9EAY5BSErCZ8qBDMVbq88h4UxaNPlbrKqfWP8RfJA==", "cpu": [ "ppc64" ], @@ -621,9 +621,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.34.8", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.34.8.tgz", - "integrity": "sha512-xAQCAHPj8nJq1PI3z8CIZzXuXCstquz7cIOL73HHdXiRcKk8Ywwqtx2wrIy23EcTn4aZ2fLJNBB8d0tQENPCmw==", + "version": "4.34.9", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.34.9.tgz", + "integrity": "sha512-Z2i0Uy5G96KBYKjeQFKbbsB54xFOL5/y1P5wNBsbXB8yE+At3oh0DVMjQVzCJRJSfReiB2tX8T6HUFZ2k8iaKg==", "cpu": [ "riscv64" ], @@ -634,9 +634,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.34.8", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.34.8.tgz", - "integrity": "sha512-DdePVk1NDEuc3fOe3dPPTb+rjMtuFw89gw6gVWxQFAuEqqSdDKnrwzZHrUYdac7A7dXl9Q2Vflxpme15gUWQFA==", + "version": "4.34.9", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.34.9.tgz", + "integrity": "sha512-U+5SwTMoeYXoDzJX5dhDTxRltSrIax8KWwfaaYcynuJw8mT33W7oOgz0a+AaXtGuvhzTr2tVKh5UO8GVANTxyQ==", "cpu": [ "s390x" ], @@ -660,9 +660,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.34.8", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.34.8.tgz", - "integrity": "sha512-SCXcP0ZpGFIe7Ge+McxY5zKxiEI5ra+GT3QRxL0pMMtxPfpyLAKleZODi1zdRHkz5/BhueUrYtYVgubqe9JBNQ==", + "version": "4.34.9", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.34.9.tgz", + "integrity": "sha512-cYRpV4650z2I3/s6+5/LONkjIz8MBeqrk+vPXV10ORBnshpn8S32bPqQ2Utv39jCiDcO2eJTuSlPXpnvmaIgRA==", "cpu": [ "x64" ], @@ -673,9 +673,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.34.8", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.34.8.tgz", - "integrity": "sha512-YHYsgzZgFJzTRbth4h7Or0m5O74Yda+hLin0irAIobkLQFRQd1qWmnoVfwmKm9TXIZVAD0nZ+GEb2ICicLyCnQ==", + "version": "4.34.9", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.34.9.tgz", + "integrity": "sha512-z4mQK9dAN6byRA/vsSgQiPeuO63wdiDxZ9yg9iyX2QTzKuQM7T4xlBoeUP/J8uiFkqxkcWndWi+W7bXdPbt27Q==", "cpu": [ "arm64" ], @@ -686,9 +686,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.34.8", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.34.8.tgz", - "integrity": "sha512-r3NRQrXkHr4uWy5TOjTpTYojR9XmF0j/RYgKCef+Ag46FWUTltm5ziticv8LdNsDMehjJ543x/+TJAek/xBA2w==", + "version": "4.34.9", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.34.9.tgz", + "integrity": "sha512-KB48mPtaoHy1AwDNkAJfHXvHp24H0ryZog28spEs0V48l3H1fr4i37tiyHsgKZJnCmvxsbATdZGBpbmxTE3a9w==", "cpu": [ "ia32" ], @@ -699,9 +699,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.34.8", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.34.8.tgz", - "integrity": "sha512-U0FaE5O1BCpZSeE6gBl3c5ObhePQSfk9vDRToMmTkbhCOgW4jqvtS5LGyQ76L1fH8sM0keRp4uDTsbjiUyjk0g==", + "version": "4.34.9", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.34.9.tgz", + "integrity": "sha512-AyleYRPU7+rgkMWbEh71fQlrzRfeP6SyMnRf9XX4fCdDPAJumdSBqYEcWPMzVQ4ScAl7E4oFfK0GUVn77xSwbw==", "cpu": [ "x64" ], @@ -712,42 +712,42 @@ ] }, "node_modules/@tailwindcss/node": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.0.8.tgz", - "integrity": "sha512-FKArQpbrbwv08TNT0k7ejYXpF+R8knZFAatNc0acOxbgeqLzwb86r+P3LGOjIeI3Idqe9CVkZrh4GlsJLJKkkw==", + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.0.9.tgz", + "integrity": "sha512-tOJvdI7XfJbARYhxX+0RArAhmuDcczTC46DGCEziqxzzbIaPnfYaIyRT31n4u8lROrsO7Q6u/K9bmQHL2uL1bQ==", "license": "MIT", "dependencies": { "enhanced-resolve": "^5.18.1", "jiti": "^2.4.2", - "tailwindcss": "4.0.8" + "tailwindcss": "4.0.9" } }, "node_modules/@tailwindcss/oxide": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.0.8.tgz", - "integrity": "sha512-KfMcuAu/Iw+DcV1e8twrFyr2yN8/ZDC/odIGta4wuuJOGkrkHZbvJvRNIbQNhGh7erZTYV6Ie0IeD6WC9Y8Hcw==", + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.0.9.tgz", + "integrity": "sha512-eLizHmXFqHswJONwfqi/WZjtmWZpIalpvMlNhTM99/bkHtUs6IqgI1XQ0/W5eO2HiRQcIlXUogI2ycvKhVLNcA==", "license": "MIT", "engines": { "node": ">= 10" }, "optionalDependencies": { - "@tailwindcss/oxide-android-arm64": "4.0.8", - "@tailwindcss/oxide-darwin-arm64": "4.0.8", - "@tailwindcss/oxide-darwin-x64": "4.0.8", - "@tailwindcss/oxide-freebsd-x64": "4.0.8", - "@tailwindcss/oxide-linux-arm-gnueabihf": "4.0.8", - "@tailwindcss/oxide-linux-arm64-gnu": "4.0.8", - "@tailwindcss/oxide-linux-arm64-musl": "4.0.8", - "@tailwindcss/oxide-linux-x64-gnu": "4.0.8", - "@tailwindcss/oxide-linux-x64-musl": "4.0.8", - "@tailwindcss/oxide-win32-arm64-msvc": "4.0.8", - "@tailwindcss/oxide-win32-x64-msvc": "4.0.8" + "@tailwindcss/oxide-android-arm64": "4.0.9", + "@tailwindcss/oxide-darwin-arm64": "4.0.9", + "@tailwindcss/oxide-darwin-x64": "4.0.9", + "@tailwindcss/oxide-freebsd-x64": "4.0.9", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.0.9", + "@tailwindcss/oxide-linux-arm64-gnu": "4.0.9", + "@tailwindcss/oxide-linux-arm64-musl": "4.0.9", + "@tailwindcss/oxide-linux-x64-gnu": "4.0.9", + "@tailwindcss/oxide-linux-x64-musl": "4.0.9", + "@tailwindcss/oxide-win32-arm64-msvc": "4.0.9", + "@tailwindcss/oxide-win32-x64-msvc": "4.0.9" } }, "node_modules/@tailwindcss/oxide-android-arm64": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.0.8.tgz", - "integrity": "sha512-We7K79+Sm4mwJHk26Yzu/GAj7C7myemm7PeXvpgMxyxO70SSFSL3uCcqFbz9JA5M5UPkrl7N9fkBe/Y0iazqpA==", + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.0.9.tgz", + "integrity": "sha512-YBgy6+2flE/8dbtrdotVInhMVIxnHJPbAwa7U1gX4l2ThUIaPUp18LjB9wEH8wAGMBZUb//SzLtdXXNBHPUl6Q==", "cpu": [ "arm64" ], @@ -761,9 +761,9 @@ } }, "node_modules/@tailwindcss/oxide-darwin-arm64": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.0.8.tgz", - "integrity": "sha512-Lv9Isi2EwkCTG1sRHNDi0uRNN1UGFdEThUAGFrydRmQZnraGLMjN8gahzg2FFnOizDl7LB2TykLUuiw833DSNg==", + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.0.9.tgz", + "integrity": "sha512-pWdl4J2dIHXALgy2jVkwKBmtEb73kqIfMpYmcgESr7oPQ+lbcQ4+tlPeVXaSAmang+vglAfFpXQCOvs/aGSqlw==", "cpu": [ "arm64" ], @@ -777,9 +777,9 @@ } }, "node_modules/@tailwindcss/oxide-darwin-x64": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.0.8.tgz", - "integrity": "sha512-fWfywfYIlSWtKoqWTjukTHLWV3ARaBRjXCC2Eo0l6KVpaqGY4c2y8snUjp1xpxUtpqwMvCvFWFaleMoz1Vhzlw==", + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.0.9.tgz", + "integrity": "sha512-4Dq3lKp0/C7vrRSkNPtBGVebEyWt9QPPlQctxJ0H3MDyiQYvzVYf8jKow7h5QkWNe8hbatEqljMj/Y0M+ERYJg==", "cpu": [ "x64" ], @@ -793,9 +793,9 @@ } }, "node_modules/@tailwindcss/oxide-freebsd-x64": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.0.8.tgz", - "integrity": "sha512-SO+dyvjJV9G94bnmq2288Ke0BIdvrbSbvtPLaQdqjqHR83v5L2fWADyFO+1oecHo9Owsk8MxcXh1agGVPIKIqw==", + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.0.9.tgz", + "integrity": "sha512-k7U1RwRODta8x0uealtVt3RoWAWqA+D5FAOsvVGpYoI6ObgmnzqWW6pnVwz70tL8UZ/QXjeMyiICXyjzB6OGtQ==", "cpu": [ "x64" ], @@ -809,9 +809,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.0.8.tgz", - "integrity": "sha512-ZSHggWiEblQNV69V0qUK5vuAtHP+I+S2eGrKGJ5lPgwgJeAd6GjLsVBN+Mqn2SPVfYM3BOpS9jX/zVg9RWQVDQ==", + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.0.9.tgz", + "integrity": "sha512-NDDjVweHz2zo4j+oS8y3KwKL5wGCZoXGA9ruJM982uVJLdsF8/1AeKvUwKRlMBpxHt1EdWJSAh8a0Mfhl28GlQ==", "cpu": [ "arm" ], @@ -825,9 +825,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.0.8.tgz", - "integrity": "sha512-xWpr6M0OZLDNsr7+bQz+3X7zcnDJZJ1N9gtBWCtfhkEtDjjxYEp+Lr5L5nc/yXlL4MyCHnn0uonGVXy3fhxaVA==", + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.0.9.tgz", + "integrity": "sha512-jk90UZ0jzJl3Dy1BhuFfRZ2KP9wVKMXPjmCtY4U6fF2LvrjP5gWFJj5VHzfzHonJexjrGe1lMzgtjriuZkxagg==", "cpu": [ "arm64" ], @@ -841,9 +841,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-arm64-musl": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.0.8.tgz", - "integrity": "sha512-5tz2IL7LN58ssGEq7h/staD7pu/izF/KeMWdlJ86WDe2Ah46LF3ET6ZGKTr5eZMrnEA0M9cVFuSPprKRHNgjeg==", + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.0.9.tgz", + "integrity": "sha512-3eMjyTC6HBxh9nRgOHzrc96PYh1/jWOwHZ3Kk0JN0Kl25BJ80Lj9HEvvwVDNTgPg154LdICwuFLuhfgH9DULmg==", "cpu": [ "arm64" ], @@ -857,9 +857,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-x64-gnu": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.0.8.tgz", - "integrity": "sha512-KSzMkhyrxAQyY2o194NKVKU9j/c+NFSoMvnHWFaNHKi3P1lb+Vq1UC19tLHrmxSkKapcMMu69D7+G1+FVGNDXQ==", + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.0.9.tgz", + "integrity": "sha512-v0D8WqI/c3WpWH1kq/HP0J899ATLdGZmENa2/emmNjubT0sWtEke9W9+wXeEoACuGAhF9i3PO5MeyditpDCiWQ==", "cpu": [ "x64" ], @@ -873,9 +873,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-x64-musl": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.0.8.tgz", - "integrity": "sha512-yFYKG5UtHTRimjtqxUWXBgI4Tc6NJe3USjRIVdlTczpLRxq/SFwgzGl5JbatCxgSRDPBFwRrNPxq+ukfQFGdrw==", + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.0.9.tgz", + "integrity": "sha512-Kvp0TCkfeXyeehqLJr7otsc4hd/BUPfcIGrQiwsTVCfaMfjQZCG7DjI+9/QqPZha8YapLA9UoIcUILRYO7NE1Q==", "cpu": [ "x64" ], @@ -889,9 +889,9 @@ } }, "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.0.8.tgz", - "integrity": "sha512-tndGujmCSba85cRCnQzXgpA2jx5gXimyspsUYae5jlPyLRG0RjXbDshFKOheVXU4TLflo7FSG8EHCBJ0EHTKdQ==", + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.0.9.tgz", + "integrity": "sha512-m3+60T/7YvWekajNq/eexjhV8z10rswcz4BC9bioJ7YaN+7K8W2AmLmG0B79H14m6UHE571qB0XsPus4n0QVgQ==", "cpu": [ "arm64" ], @@ -905,9 +905,9 @@ } }, "node_modules/@tailwindcss/oxide-win32-x64-msvc": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.0.8.tgz", - "integrity": "sha512-T77jroAc0p4EHVVgTUiNeFn6Nj3jtD3IeNId2X+0k+N1XxfNipy81BEkYErpKLiOkNhpNFjPee8/ZVas29b2OQ==", + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.0.9.tgz", + "integrity": "sha512-dpc05mSlqkwVNOUjGu/ZXd5U1XNch1kHFJ4/cHkZFvaW1RzbHmRt24gvM8/HC6IirMxNarzVw4IXVtvrOoZtxA==", "cpu": [ "x64" ], @@ -921,15 +921,15 @@ } }, "node_modules/@tailwindcss/vite": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.0.8.tgz", - "integrity": "sha512-+SAq44yLzYlzyrb7QTcFCdU8Xa7FOA0jp+Xby7fPMUie+MY9HhJysM7Vp+vL8qIp8ceQJfLD+FjgJuJ4lL6nyg==", + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.0.9.tgz", + "integrity": "sha512-BIKJO+hwdIsN7V6I7SziMZIVHWWMsV/uCQKYEbeiGRDRld+TkqyRRl9+dQ0MCXbhcVr+D9T/qX2E84kT7V281g==", "license": "MIT", "dependencies": { - "@tailwindcss/node": "4.0.8", - "@tailwindcss/oxide": "4.0.8", + "@tailwindcss/node": "4.0.9", + "@tailwindcss/oxide": "4.0.9", "lightningcss": "^1.29.1", - "tailwindcss": "4.0.8" + "tailwindcss": "4.0.9" }, "peerDependencies": { "vite": "^5.2.0 || ^6" @@ -948,9 +948,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "22.13.5", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.5.tgz", - "integrity": "sha512-+lTU0PxZXn0Dr1NBtC7Y8cR21AJr87dLLU953CWA6pMxxv/UDc7jYAY90upcrie1nRcD6XNG5HOYEDtgW5TxAg==", + "version": "22.13.9", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.9.tgz", + "integrity": "sha512-acBjXdRJ3A6Pb3tqnw9HZmyR3Fiol3aGxRCK1x3d+6CDAMjl7I649wpSd+yNURCjbOUGu9tqtLKnTGxmK6CyGw==", "license": "MIT", "optional": true, "dependencies": { @@ -1062,9 +1062,9 @@ } }, "node_modules/axios": { - "version": "1.7.9", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.9.tgz", - "integrity": "sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw==", + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.8.1.tgz", + "integrity": "sha512-NN+fvwH/kV01dYUQ3PTOZns4LWtWhOFCAhQ/pHb88WQ1hNe5V/dvFwc4VJcDL11LT9xSX0QtsR8sWUuyOuOq7g==", "license": "MIT", "dependencies": { "follow-redirects": "^1.15.6", @@ -1101,13 +1101,13 @@ } }, "node_modules/bare-os": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.4.0.tgz", - "integrity": "sha512-9Ous7UlnKbe3fMi7Y+qh0DwAup6A1JkYgPnjvMDNOlmnxNRQvQ/7Nst+OnUQKzk0iAT0m9BisbDVp9gCv8+ETA==", + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.5.1.tgz", + "integrity": "sha512-LvfVNDcWLw2AnIw5f2mWUgumW3I3N/WYGiWeimhQC1Ybt71n2FjlS9GJKeCnFeg1MKZHxzIFmpFnBXDI+sBeFg==", "license": "Apache-2.0", "optional": true, "engines": { - "bare": ">=1.6.0" + "bare": ">=1.14.0" } }, "node_modules/bare-path": { @@ -1215,9 +1215,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001700", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001700.tgz", - "integrity": "sha512-2S6XIXwaE7K7erT8dY+kLQcpa5ms63XlRkMkReXjle+kf6c5g38vyMl+Z5y8dSxOFDhcFe+nxnn261PLxBSQsQ==", + "version": "1.0.30001702", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001702.tgz", + "integrity": "sha512-LoPe/D7zioC0REI5W73PeR1e1MLCipRGq/VkovJnd6Df+QVqT+vT33OXCp8QUd7kA7RZrHWxb1B36OQKI/0gOA==", "funding": [ { "type": "opencollective", @@ -1263,9 +1263,9 @@ } }, "node_modules/chromium-bidi": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-2.0.0.tgz", - "integrity": "sha512-8VmyVj0ewSY4pstZV0Y3rCUUwpomam8uWgHZf1XavRxJEP4vU9/dcpNuoyB+u4AQxPo96CASXz5CHPvdH+dSeQ==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-2.1.2.tgz", + "integrity": "sha512-vtRWBK2uImo5/W2oG6/cDkkHSm+2t6VHgnj+Rcwhb0pP74OoUb4GipyRX/T/y39gYQPhioP0DPShn+A7P6CHNw==", "license": "Apache-2.0", "dependencies": { "mitt": "^3.0.1", @@ -1452,9 +1452,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.104", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.104.tgz", - "integrity": "sha512-Us9M2L4cO/zMBqVkJtnj353nQhMju9slHm62NprKTmdF3HH8wYOtNvDFq/JB2+ZRoGLzdvYDiATlMHs98XBM1g==", + "version": "1.5.112", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.112.tgz", + "integrity": "sha512-oen93kVyqSb3l+ziUgzIOlWt/oOuy4zRmpwestMn4rhFWAoFJeFuCVte9F2fASjeZZo7l/Cif9TiyrdW4CwEMA==", "license": "ISC" }, "node_modules/emoji-regex": { @@ -2531,17 +2531,17 @@ } }, "node_modules/puppeteer": { - "version": "24.3.0", - "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-24.3.0.tgz", - "integrity": "sha512-wYEx+NnEM1T6ncHB+IsTovUgx+JlZ0pv0sRGTb8IzoTeOILvyUcdU2h34bYEQ1iG5maz1VQA5eI4kzIyAVh90A==", + "version": "24.3.1", + "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-24.3.1.tgz", + "integrity": "sha512-k0OJ7itRwkr06owp0CP3f/PsRD7Pdw4DjoCUZvjGr+aNgS1z6n/61VajIp0uBjl+V5XAQO1v/3k9bzeZLWs9OQ==", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { "@puppeteer/browsers": "2.7.1", - "chromium-bidi": "2.0.0", + "chromium-bidi": "2.1.2", "cosmiconfig": "^9.0.0", "devtools-protocol": "0.0.1402036", - "puppeteer-core": "24.3.0", + "puppeteer-core": "24.3.1", "typed-query-selector": "^2.12.0" }, "bin": { @@ -2552,13 +2552,13 @@ } }, "node_modules/puppeteer-core": { - "version": "24.3.0", - "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-24.3.0.tgz", - "integrity": "sha512-x8kQRP/xxtiFav6wWuLzrctO0HWRpSQy+JjaHbqIl+d5U2lmRh2pY9vh5AzDFN0EtOXW2pzngi9RrryY1vZGig==", + "version": "24.3.1", + "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-24.3.1.tgz", + "integrity": "sha512-585ccfcTav4KmlSmYbwwOSeC8VdutQHn2Fuk0id/y/9OoeO7Gg5PK1aUGdZjEmos0TAq+pCpChqFurFbpNd3wA==", "license": "Apache-2.0", "dependencies": { "@puppeteer/browsers": "2.7.1", - "chromium-bidi": "2.0.0", + "chromium-bidi": "2.1.2", "debug": "^4.4.0", "devtools-protocol": "0.0.1402036", "typed-query-selector": "^2.12.0", @@ -2587,9 +2587,9 @@ } }, "node_modules/rollup": { - "version": "4.34.8", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.34.8.tgz", - "integrity": "sha512-489gTVMzAYdiZHFVA/ig/iYFllCcWFHMvUHI1rpFmkoUtRlQxqh6/yiNqnYibjMZ2b/+FUQwldG+aLsEt6bglQ==", + "version": "4.34.9", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.34.9.tgz", + "integrity": "sha512-nF5XYqWWp9hx/LrpC8sZvvvmq0TeTjQgaZHYmAgwysT9nh8sWnZhBnM8ZyVbbJFIQBLwHDNoMqsBZBbUo4U8sQ==", "license": "MIT", "dependencies": { "@types/estree": "1.0.6" @@ -2602,32 +2602,32 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.34.8", - "@rollup/rollup-android-arm64": "4.34.8", - "@rollup/rollup-darwin-arm64": "4.34.8", - "@rollup/rollup-darwin-x64": "4.34.8", - "@rollup/rollup-freebsd-arm64": "4.34.8", - "@rollup/rollup-freebsd-x64": "4.34.8", - "@rollup/rollup-linux-arm-gnueabihf": "4.34.8", - "@rollup/rollup-linux-arm-musleabihf": "4.34.8", - "@rollup/rollup-linux-arm64-gnu": "4.34.8", - "@rollup/rollup-linux-arm64-musl": "4.34.8", - "@rollup/rollup-linux-loongarch64-gnu": "4.34.8", - "@rollup/rollup-linux-powerpc64le-gnu": "4.34.8", - "@rollup/rollup-linux-riscv64-gnu": "4.34.8", - "@rollup/rollup-linux-s390x-gnu": "4.34.8", - "@rollup/rollup-linux-x64-gnu": "4.34.8", - "@rollup/rollup-linux-x64-musl": "4.34.8", - "@rollup/rollup-win32-arm64-msvc": "4.34.8", - "@rollup/rollup-win32-ia32-msvc": "4.34.8", - "@rollup/rollup-win32-x64-msvc": "4.34.8", + "@rollup/rollup-android-arm-eabi": "4.34.9", + "@rollup/rollup-android-arm64": "4.34.9", + "@rollup/rollup-darwin-arm64": "4.34.9", + "@rollup/rollup-darwin-x64": "4.34.9", + "@rollup/rollup-freebsd-arm64": "4.34.9", + "@rollup/rollup-freebsd-x64": "4.34.9", + "@rollup/rollup-linux-arm-gnueabihf": "4.34.9", + "@rollup/rollup-linux-arm-musleabihf": "4.34.9", + "@rollup/rollup-linux-arm64-gnu": "4.34.9", + "@rollup/rollup-linux-arm64-musl": "4.34.9", + "@rollup/rollup-linux-loongarch64-gnu": "4.34.9", + "@rollup/rollup-linux-powerpc64le-gnu": "4.34.9", + "@rollup/rollup-linux-riscv64-gnu": "4.34.9", + "@rollup/rollup-linux-s390x-gnu": "4.34.9", + "@rollup/rollup-linux-x64-gnu": "4.34.9", + "@rollup/rollup-linux-x64-musl": "4.34.9", + "@rollup/rollup-win32-arm64-msvc": "4.34.9", + "@rollup/rollup-win32-ia32-msvc": "4.34.9", + "@rollup/rollup-win32-x64-msvc": "4.34.9", "fsevents": "~2.3.2" } }, "node_modules/rollup/node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.34.8", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.34.8.tgz", - "integrity": "sha512-8y7ED8gjxITUltTUEJLQdgpbPh1sUQ0kMTmufRF/Ns5tI9TNMNlhWtmPKKHCU0SilX+3MJkZ0zERYYGIVBYHIA==", + "version": "4.34.9", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.34.9.tgz", + "integrity": "sha512-FwBHNSOjUTQLP4MG7y6rR6qbGw4MFeQnIBrMe161QGaQoBQLqSUEKlHIiVgF3g/mb3lxlxzJOpIBhaP+C+KP2A==", "cpu": [ "x64" ], @@ -2788,9 +2788,9 @@ } }, "node_modules/tailwindcss": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.0.8.tgz", - "integrity": "sha512-Me7N5CKR+D2A1xdWA5t5+kjjT7bwnxZOE6/yDI/ixJdJokszsn2n++mdU5yJwrsTpqFX2B9ZNMBJDwcqk9C9lw==", + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.0.9.tgz", + "integrity": "sha512-12laZu+fv1ONDRoNR9ipTOpUD7RN9essRVkX36sjxuRUInpN7hIiHN4lBd/SIFjbISvnXzp8h/hXzmU8SQQYhw==", "license": "MIT" }, "node_modules/tapable": { @@ -2865,9 +2865,9 @@ "optional": true }, "node_modules/update-browserslist-db": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.2.tgz", - "integrity": "sha512-PPypAm5qvlD7XMZC3BujecnaOxwhrtoFR+Dqkk5Aa/6DssiH0ibKoketaj9w8LP7Bont1rYeoV5plxD7RTEPRg==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", + "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", "funding": [ { "type": "opencollective", diff --git a/resources/views/components/app-logo-icon.blade.php b/resources/views/components/app-logo-icon.blade.php index 4b23826..39862a2 100644 --- a/resources/views/components/app-logo-icon.blade.php +++ b/resources/views/components/app-logo-icon.blade.php @@ -1,16 +1,33 @@ - - - - - - - - - - - - - - - - +@if(config('services.trmnl.override_orig_icon')) + + + + + + + + + + + + + + + + +@else + +@endif diff --git a/resources/views/livewire/device-dashboard.blade.php b/resources/views/livewire/device-dashboard.blade.php index 56d7244..7afe67b 100644 --- a/resources/views/livewire/device-dashboard.blade.php +++ b/resources/views/livewire/device-dashboard.blade.php @@ -11,42 +11,44 @@ new class extends Component { ?>
-
- @if($devices->isEmpty()) -
-
-
-

Add your first device

- Add Device - +
+
+ @if($devices->isEmpty()) +
+
+
+

Add your first device

+ Add Device + +
-
- @endif + @endif - @foreach($devices as $device) -
-
-
- @php - $current_image_uuid =$device->current_screen_image; - file_exists('storage/images/generated/' . $current_image_uuid . '.png') ? $file_extension = 'png' : $file_extension = 'bmp'; - $current_image_path = 'storage/images/generated/' . $current_image_uuid . '.' . $file_extension; - @endphp + @foreach($devices as $device) +
+
+
+ @php + $current_image_uuid =$device->current_screen_image; + file_exists('storage/images/generated/' . $current_image_uuid . '.png') ? $file_extension = 'png' : $file_extension = 'bmp'; + $current_image_path = 'storage/images/generated/' . $current_image_uuid . '.' . $file_extension; + @endphp -

{{ $device->name }}

-

{{$device->mac_address}}

- @if($current_image_uuid) - - Current Image - @endif +

{{ $device->name }}

+

{{$device->mac_address}}

+ @if($current_image_uuid) + + Current Image + @endif +
-
- @endforeach + @endforeach +
{{-- @php--}} diff --git a/routes/console.php b/routes/console.php index b4cff25..d5cb361 100644 --- a/routes/console.php +++ b/routes/console.php @@ -2,8 +2,4 @@ use App\Jobs\FetchProxyCloudResponses; -// Artisan::command('inspire', function () { -// $this->comment(Inspiring::quote()); -// })->purpose('Display an inspiring quote')->hourly(); - -Schedule::job(new FetchProxyCloudResponses)->everyFifteenMinutes(); +Schedule::job(FetchProxyCloudResponses::class, [])->cron(sprintf('*/%s * * * *', intval(config('services.trmnl.proxy_refresh_minutes', 15)))); diff --git a/screenshots/README_byos-screenshot2.png b/screenshots/README_byos-screenshot2.png new file mode 100644 index 0000000..e06257e Binary files /dev/null and b/screenshots/README_byos-screenshot2.png differ diff --git a/screenshots/README_byos-screenshot3.png b/screenshots/README_byos-screenshot3.png new file mode 100644 index 0000000..4012305 Binary files /dev/null and b/screenshots/README_byos-screenshot3.png differ diff --git a/screenshots/README_byos-screenshot4.png b/screenshots/README_byos-screenshot4.png new file mode 100644 index 0000000..fdaf074 Binary files /dev/null and b/screenshots/README_byos-screenshot4.png differ diff --git a/screenshots/README_byos-screenshot5.png b/screenshots/README_byos-screenshot5.png new file mode 100644 index 0000000..141e5e9 Binary files /dev/null and b/screenshots/README_byos-screenshot5.png differ diff --git a/screenshots/README_byos-screenshot6.png b/screenshots/README_byos-screenshot6.png new file mode 100644 index 0000000..9fcc8de Binary files /dev/null and b/screenshots/README_byos-screenshot6.png differ diff --git a/screenshots/SCREENSHOTS.md b/screenshots/SCREENSHOTS.md new file mode 100644 index 0000000..76e9661 --- /dev/null +++ b/screenshots/SCREENSHOTS.md @@ -0,0 +1,12 @@ +## Sceenshots + + +![Screenshot](README_byos-screenshot2.png) + +![Screenshot](README_byos-screenshot3.png) + +![Screenshot](README_byos-screenshot4.png) + +![Screenshot](README_byos-screenshot5.png) + +![Screenshot](README_byos-screenshot6.png) diff --git a/tests/Feature/Api/DeviceEndpointsTest.php b/tests/Feature/Api/DeviceEndpointsTest.php new file mode 100644 index 0000000..62b8364 --- /dev/null +++ b/tests/Feature/Api/DeviceEndpointsTest.php @@ -0,0 +1,158 @@ +create([ + 'mac_address' => '00:11:22:33:44:55', + 'api_key' => 'test-api-key', + 'current_screen_image' => 'test-image', + ]); + + $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([ + 'status' => '0', + 'filename' => 'test-image.bmp', + 'refresh_rate' => 900, + 'reset_firmware' => false, + 'update_firmware' => false, + 'firmware_url' => null, + 'special_function' => 'sleep', + ]); + + expect($device->fresh()) + ->last_rssi_level->toBe(-70) + ->last_battery_voltage->toBe(3.8) + ->last_firmware_version->toBe('1.0.0'); +}); + +test('new device is auto-assigned to user with auto-assign enabled', function () { + $user = User::factory()->create(['assign_new_devices' => true]); + + $response = $this->withHeaders([ + 'id' => '00:11:22:33:44:55', + 'access-token' => 'new-device-key', + 'rssi' => -70, + 'battery_voltage' => 3.8, + 'fw-version' => '1.0.0', + ])->get('/api/display'); + + $response->assertOk(); + + $device = Device::where('mac_address', '00:11:22:33:44:55')->first(); + expect($device) + ->not->toBeNull() + ->user_id->toBe($user->id) + ->api_key->toBe('new-device-key'); +}); + +test('device setup endpoint returns correct data', function () { + $device = Device::factory()->create([ + 'mac_address' => '00:11:22:33:44:55', + 'api_key' => 'test-api-key', + 'friendly_id' => 'test-device', + ]); + + $response = $this->withHeaders([ + 'id' => $device->mac_address, + ])->get('/api/setup'); + + $response->assertOk() + ->assertJson([ + 'api_key' => 'test-api-key', + 'friendly_id' => 'test-device', + 'message' => 'Welcome to TRMNL BYOS', + ]); +}); + +test('device can submit logs', function () { + $device = Device::factory()->create([ + 'mac_address' => '00:11:22:33:44:55', + 'api_key' => 'test-api-key', + ]); + + $logData = [ + 'log' => [ + 'logs_array' => [ + ['message' => 'Test log message', 'level' => 'info'], + ], + ], + ]; + + $response = $this->withHeaders([ + 'id' => $device->mac_address, + 'access-token' => $device->api_key, + ])->postJson('/api/log', $logData); + + $response->assertOk() + ->assertJson(['status' => '0']); + + expect($device->fresh()->last_log_request) + ->toBe($logData); +}); + +// test('authenticated user can update device display', function () { +// $user = User::factory()->create(); +// $device = Device::factory()->create(['user_id' => $user->id]); +// +// Sanctum::actingAs($user, ['update-screen']); +// +// $response = $this->postJson('/api/display/update', [ +// 'device_id' => $device->id, +// 'markup' => '
Test markup
' +// ]); +// +// $response->assertOk(); +// }); + +test('user cannot update display for devices they do not own', function () { + $user = User::factory()->create(); + $otherUser = User::factory()->create(); + $device = Device::factory()->create(['user_id' => $otherUser->id]); + + Sanctum::actingAs($user, ['update-screen']); + + $response = $this->postJson('/api/display/update', [ + 'device_id' => $device->id, + 'markup' => '
Test markup
', + ]); + + $response->assertForbidden(); +}); + +test('invalid device credentials return error', function () { + $response = $this->withHeaders([ + 'id' => 'invalid-mac', + 'access-token' => 'invalid-token', + ])->get('/api/display'); + + $response->assertNotFound() + ->assertJson(['message' => 'MAC Address not registered or invalid access token']); +}); + +test('log endpoint requires valid device credentials', function () { + $response = $this->withHeaders([ + 'id' => 'invalid-mac', + 'access-token' => 'invalid-token', + ])->postJson('/api/log', ['log' => []]); + + $response->assertNotFound() + ->assertJson(['message' => 'Device not found or invalid access token']); +}); diff --git a/tests/Feature/Console/FetchProxyCloudResponsesCommandTest.php b/tests/Feature/Console/FetchProxyCloudResponsesCommandTest.php new file mode 100644 index 0000000..b34357d --- /dev/null +++ b/tests/Feature/Console/FetchProxyCloudResponsesCommandTest.php @@ -0,0 +1,15 @@ +artisan('trmnl:cloud:proxy')->assertSuccessful(); + + // Assert that the job was dispatched + Bus::assertDispatched(FetchProxyCloudResponses::class); +}); diff --git a/tests/Feature/Console/ScreenGeneratorCommandTest.php b/tests/Feature/Console/ScreenGeneratorCommandTest.php new file mode 100644 index 0000000..54621d6 --- /dev/null +++ b/tests/Feature/Console/ScreenGeneratorCommandTest.php @@ -0,0 +1,13 @@ +artisan('trmnl:screen:generate') + ->assertSuccessful(); + + Bus::assertDispatched(GenerateScreenJob::class); +}); diff --git a/tests/Feature/Devices/DeviceTest.php b/tests/Feature/Devices/DeviceTest.php new file mode 100644 index 0000000..ad12879 --- /dev/null +++ b/tests/Feature/Devices/DeviceTest.php @@ -0,0 +1,76 @@ +create([ + 'name' => 'Test Device', + ]); + + expect($device)->toBeInstanceOf(Device::class) + ->and($device->name)->toBe('Test Device'); +}); + +test('battery percentage is calculated correctly', function () { + $cases = [ + ['voltage' => 3.0, 'expected' => 0], // Min voltage + ['voltage' => 4.2, 'expected' => 100], // Max voltage + ['voltage' => 2.9, 'expected' => 0], // Below min + ['voltage' => 4.3, 'expected' => 100], // Above max + ['voltage' => 3.6, 'expected' => 50.0], // Middle voltage + ['voltage' => 3.3, 'expected' => 25.0], // Quarter voltage + ]; + + foreach ($cases as $case) { + $device = Device::factory()->create([ + 'last_battery_voltage' => $case['voltage'], + ]); + + expect($device->battery_percent)->toBe($case['expected']) + ->and($device->last_battery_voltage)->toBe($case['voltage']); + } +}); + +test('wifi strength is determined correctly', function () { + $cases = [ + ['rssi' => 0, 'expected' => 0], // No signal + ['rssi' => -90, 'expected' => 1], // Weak signal + ['rssi' => -70, 'expected' => 2], // Moderate signal + ['rssi' => -50, 'expected' => 3], // Strong signal + ]; + + foreach ($cases as $case) { + $device = Device::factory()->create([ + 'last_rssi_level' => $case['rssi'], + ]); + + expect($device->wifi_strengh)->toBe($case['expected']) + ->and($device->last_rssi_level)->toBe($case['rssi']); + } +}); + +test('proxy cloud attribute is properly cast to boolean', function () { + $device = Device::factory()->create([ + 'proxy_cloud' => true, + ]); + + expect($device->proxy_cloud)->toBeTrue(); + + $device->update(['proxy_cloud' => false]); + expect($device->proxy_cloud)->toBeFalse(); +}); + +test('last log request is properly cast to json', function () { + $logData = ['status' => 'success', 'timestamp' => '2024-03-04 12:00:00']; + + $device = Device::factory()->create([ + 'last_log_request' => $logData, + ]); + + expect($device->last_log_request) + ->toBeArray() + ->toHaveKey('status') + ->toHaveKey('timestamp'); +}); diff --git a/tests/Feature/Devices/ManageTest.php b/tests/Feature/Devices/ManageTest.php new file mode 100644 index 0000000..b54d6a8 --- /dev/null +++ b/tests/Feature/Devices/ManageTest.php @@ -0,0 +1,106 @@ +create(); + + $response = $this->actingAs($user) + ->get('/devices'); + + $response->assertOk(); +}); + +test('user can create a new device', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $deviceData = [ + 'name' => 'Test Device', + 'mac_address' => '00:11:22:33:44:55', + 'api_key' => 'test-api-key', + 'default_refresh_interval' => 900, + 'friendly_id' => 'test-device-1', + ]; + + $response = Volt::test('devices.manage') + ->set('name', $deviceData['name']) + ->set('mac_address', $deviceData['mac_address']) + ->set('api_key', $deviceData['api_key']) + ->set('default_refresh_interval', $deviceData['default_refresh_interval']) + ->set('friendly_id', $deviceData['friendly_id']) + ->call('createDevice'); + + $response->assertHasNoErrors(); + + expect(Device::count())->toBe(1); + + $device = Device::first(); + expect($device->name)->toBe($deviceData['name']); + expect($device->mac_address)->toBe($deviceData['mac_address']); + expect($device->api_key)->toBe($deviceData['api_key']); + expect($device->default_refresh_interval)->toBe($deviceData['default_refresh_interval']); + expect($device->friendly_id)->toBe($deviceData['friendly_id']); + expect($device->user_id)->toBe($user->id); +}); + +test('device creation requires required fields', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $response = Volt::test('devices.manage') + ->set('name', '') + ->set('mac_address', '') + ->set('api_key', '') + ->set('default_refresh_interval', '') + ->set('friendly_id', '') + ->call('createDevice'); + + $response->assertHasErrors([ + 'mac_address', + 'api_key', + 'default_refresh_interval', + ]); +}); + +test('user can toggle proxy cloud for their device', function () { + $user = User::factory()->create(); + $this->actingAs($user); + $device = Device::factory()->create([ + 'user_id' => $user->id, + 'proxy_cloud' => false, + ]); + + $response = Volt::test('devices.manage') + ->call('toggleProxyCloud', $device); + + $response->assertHasNoErrors(); + expect($device->fresh()->proxy_cloud)->toBeTrue(); + + // Toggle back to false + $response = Volt::test('devices.manage') + ->call('toggleProxyCloud', $device); + + expect($device->fresh()->proxy_cloud)->toBeFalse(); +}); + +test('user cannot toggle proxy cloud for other users devices', function () { + $user = User::factory()->create(); + $this->actingAs($user); + + $otherUser = User::factory()->create(); + $device = Device::factory()->create([ + 'user_id' => $otherUser->id, + 'proxy_cloud' => false, + ]); + + $response = Volt::test('devices.manage') + ->call('toggleProxyCloud', $device); + + $response->assertStatus(403); + expect($device->fresh()->proxy_cloud)->toBeFalse(); +}); diff --git a/tests/Feature/FetchProxyCloudResponsesTest.php b/tests/Feature/FetchProxyCloudResponsesTest.php new file mode 100644 index 0000000..c9a3b63 --- /dev/null +++ b/tests/Feature/FetchProxyCloudResponsesTest.php @@ -0,0 +1,140 @@ +makeDirectory('/images/generated'); +}); + +test('it fetches and processes proxy cloud responses for devices', function () { + config(['services.trmnl.proxy_base_url' => 'https://example.com']); + + // Create a test device with proxy cloud enabled + $device = Device::factory()->create([ + 'proxy_cloud' => true, + 'mac_address' => '00:11:22:33:44:55', + 'api_key' => 'test-api-key', + 'last_rssi_level' => -70, + 'last_battery_voltage' => 3.7, + 'default_refresh_interval' => 300, + 'last_firmware_version' => '1.0.0', + ]); + + // Mock the API response + Http::fake([ + config('services.trmnl.proxy_base_url').'/api/display' => Http::response([ + 'image_url' => 'https://example.com/test-image.bmp', + 'filename' => 'test-image', + ]), + 'https://example.com/test-image.bmp' => Http::response('fake-image-content'), + ]); + + Http::withHeaders([ + 'id' => $device->mac_address, + 'access-token' => $device->api_key, + 'width' => 800, + 'height' => 480, + 'rssi' => $device->last_rssi_level, + 'battery_voltage' => $device->last_battery_voltage, + 'refresh-rate' => $device->default_refresh_interval, + 'fw-version' => $device->last_firmware_version, + 'accept-encoding' => 'identity;q=1,chunked;q=0.1,*;q=0', + 'user-agent' => 'ESP32HTTPClient', + ])->get(config('services.trmnl.proxy_base_url').'/api/display'); + + // Run the job + $job = new FetchProxyCloudResponses; + $job->handle(); + + // Assert HTTP requests were made with correct headers + Http::assertSent(function ($request) use ($device) { + return $request->hasHeader('id', $device->mac_address) && + $request->hasHeader('access-token', $device->api_key) && + $request->hasHeader('width', 800) && + $request->hasHeader('height', 480) && + $request->hasHeader('rssi', $device->last_rssi_level) && + $request->hasHeader('battery_voltage', $device->last_battery_voltage) && + $request->hasHeader('refresh-rate', $device->default_refresh_interval) && + $request->hasHeader('fw-version', $device->last_firmware_version); + }); + // Assert the device was updated + $device->refresh(); + + expect($device->current_screen_image)->toBe('test-image') + ->and($device->proxy_cloud_response)->toBe('{"image_url":"https:\\/\\/example.com\\/test-image.bmp","filename":"test-image"}'); + + // Assert the image was saved + Storage::disk('public')->assertExists('images/generated/test-image.bmp'); +}); + +test('it handles log requests when present', function () { + $device = Device::factory()->create([ + 'proxy_cloud' => true, + 'mac_address' => '00:11:22:33:44:55', + 'api_key' => 'test-api-key', + 'last_log_request' => ['message' => 'test log'], + ]); + + Http::fake([ + config('services.trmnl.proxy_base_url').'/api/display' => Http::response([ + 'image_url' => 'https://example.com/test-image.bmp', + 'filename' => 'test-image', + ]), + 'https://example.com/test-image.bmp' => Http::response('fake-image-content'), + config('services.trmnl.proxy_base_url').'/api/log' => Http::response(null, 200), + ]); + + $job = new FetchProxyCloudResponses; + $job->handle(); + + // Assert log request was sent + Http::assertSent(function ($request) use ($device) { + return $request->url() === config('services.trmnl.proxy_base_url').'/api/log' && + $request->hasHeader('id', $device->mac_address) && + $request->body() === json_encode(['message' => 'test log']); + }); + + // Assert log request was cleared + $device->refresh(); + expect($device->last_log_request)->toBeNull(); +}); + +test('it handles API errors gracefully', function () { + $device = Device::factory()->create([ + 'proxy_cloud' => true, + 'mac_address' => '00:11:22:33:44:55', + ]); + + Http::fake([ + config('services.trmnl.proxy_base_url').'/api/display' => Http::response(null, 500), + ]); + + $job = new FetchProxyCloudResponses; + + // Job should not throw exception but log error + expect(fn () => $job->handle())->not->toThrow(Exception::class); +}); + +test('it only processes proxy cloud enabled devices', function () { + Http::fake(); + $enabledDevice = Device::factory()->create(['proxy_cloud' => true]); + $disabledDevice = Device::factory()->create(['proxy_cloud' => false]); + + $job = new FetchProxyCloudResponses; + $job->handle(); + + // Assert request was only made for enabled device + Http::assertSent(function ($request) use ($enabledDevice) { + return $request->hasHeader('id', $enabledDevice->mac_address); + }); + + Http::assertNotSent(function ($request) use ($disabledDevice) { + return $request->hasHeader('id', $disabledDevice->mac_address); + }); +}); diff --git a/tests/Feature/GenerateScreenJobTest.php b/tests/Feature/GenerateScreenJobTest.php new file mode 100644 index 0000000..e1f24ab --- /dev/null +++ b/tests/Feature/GenerateScreenJobTest.php @@ -0,0 +1,59 @@ +makeDirectory('/images/generated'); +}); + +test('it generates screen images and updates device', function () { + $device = Device::factory()->create(); + $job = new GenerateScreenJob($device->id, view('trmnl')->render()); + $job->handle(); + + // Assert the device was updated with a new image UUID + $device->refresh(); + expect($device->current_screen_image)->not->toBeNull(); + + // Assert both PNG and BMP files were created + $uuid = $device->current_screen_image; + Storage::disk('public')->assertExists("/images/generated/{$uuid}.png"); + Storage::disk('public')->assertExists("/images/generated/{$uuid}.bmp"); +})->skipOnGitHubActions(); + +test('it cleans up unused images', function () { + // Create some test devices with images + $activeDevice = Device::factory()->create([ + 'current_screen_image' => 'uuid-to-be-replaced', + ]); + + // Create some test files + Storage::disk('public')->put('/images/generated/uuid-to-be-replaced.png', 'test'); + Storage::disk('public')->put('/images/generated/uuid-to-be-replaced.bmp', 'test'); + Storage::disk('public')->put('/images/generated/inactive-uuid.png', 'test'); + Storage::disk('public')->put('/images/generated/inactive-uuid.bmp', 'test'); + + // Run a job which will trigger cleanup + $job = new GenerateScreenJob($activeDevice->id, '
Test
'); + $job->handle(); + + Storage::disk('public')->assertMissing('/images/generated/uuid-to-be-replaced.png'); + Storage::disk('public')->assertMissing('/images/generated/uuid-to-be-replaced.bmp'); + Storage::disk('public')->assertMissing('/images/generated/inactive-uuid.png'); + Storage::disk('public')->assertMissing('/images/generated/inactive-uuid.bmp'); +})->skipOnGitHubActions(); + +test('it preserves gitignore file during cleanup', function () { + Storage::disk('public')->put('/images/generated/.gitignore', '*'); + + $device = Device::factory()->create(); + $job = new GenerateScreenJob($device->id, '
Test
'); + $job->handle(); + + Storage::disk('public')->assertExists('/images/generated/.gitignore'); +})->skipOnGitHubActions(); diff --git a/tests/Pest.php b/tests/Pest.php index 40d096b..b93193e 100644 --- a/tests/Pest.php +++ b/tests/Pest.php @@ -15,6 +15,7 @@ pest()->extend(Tests\TestCase::class) ->use(Illuminate\Foundation\Testing\RefreshDatabase::class) ->in('Feature'); +registerSpatiePestHelpers(); /* |-------------------------------------------------------------------------- | Expectations @@ -26,9 +27,9 @@ pest()->extend(Tests\TestCase::class) | */ -expect()->extend('toBeOne', function () { - return $this->toBe(1); -}); +// expect()->extend('toBeOne', function () { +// return $this->toBe(1); +// }); /* |-------------------------------------------------------------------------- @@ -41,7 +42,7 @@ expect()->extend('toBeOne', function () { | */ -function something() -{ - // .. -} +// function something() +// { +// // .. +// }