feat: add tests, chore: update readme

This commit is contained in:
Benjamin Nussbaum 2025-03-03 22:20:52 +01:00
parent 715e6a2562
commit e6a2bdb3bc
27 changed files with 1179 additions and 299 deletions

51
.github/workflows/test.yml vendored Normal file
View file

@ -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

191
README.md
View file

@ -1,10 +1,24 @@
## Laravel Trmnl Server ## 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) ![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 ### Requirements
* PHP >= 8.2 * PHP >= 8.2
* ext-imagick * ext-imagick
* puppeteer [see Browsershot docs](https://spatie.be/docs/browsershot/v4/requirements) * 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 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 ### 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 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 1. Switch on the “Permit Auto-Join” toggle in the header. For that to work only one user can be registered.
* You can grab the TRMNL Mac Address and API Key from the TRMNL Dashboard. Or debug the incoming request to `/api/setup` to determine. 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) 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 * To generate the screen, run
```bash ```bash
php artisan trmnl:screen:generate 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 <TOKEN>`
##### Body
```json
{
"markup": "<h1>Hello World</h1>"
}
```
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 Laravels 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 youd 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 ### License
MIT MIT

Binary file not shown.

Before

Width:  |  Height:  |  Size: 148 KiB

After

Width:  |  Height:  |  Size: 210 KiB

Before After
Before After

View file

@ -19,7 +19,8 @@
"laravel/tinker": "^2.10.1", "laravel/tinker": "^2.10.1",
"livewire/flux": "^2.0", "livewire/flux": "^2.0",
"livewire/volt": "^1.6.7", "livewire/volt": "^1.6.7",
"spatie/browsershot": "^5.0" "spatie/browsershot": "^5.0",
"spatie/pest-expectations": "^1.3"
}, },
"require-dev": { "require-dev": {
"fakerphp/faker": "^1.23", "fakerphp/faker": "^1.23",

155
composer.lock generated
View file

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "2820695c16f3a51b2464c1ed739338ba", "content-hash": "a37113926b52744df508a5619719b4dc",
"packages": [ "packages": [
{ {
"name": "bnussbau/laravel-trmnl", "name": "bnussbau/laravel-trmnl",
@ -87,16 +87,16 @@
}, },
{ {
"name": "brick/math", "name": "brick/math",
"version": "0.12.2", "version": "0.12.3",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/brick/math.git", "url": "https://github.com/brick/math.git",
"reference": "901eddb1e45a8e0f689302e40af871c181ecbe40" "reference": "866551da34e9a618e64a819ee1e01c20d8a588ba"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/brick/math/zipball/901eddb1e45a8e0f689302e40af871c181ecbe40", "url": "https://api.github.com/repos/brick/math/zipball/866551da34e9a618e64a819ee1e01c20d8a588ba",
"reference": "901eddb1e45a8e0f689302e40af871c181ecbe40", "reference": "866551da34e9a618e64a819ee1e01c20d8a588ba",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -135,7 +135,7 @@
], ],
"support": { "support": {
"issues": "https://github.com/brick/math/issues", "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": [ "funding": [
{ {
@ -143,7 +143,7 @@
"type": "github" "type": "github"
} }
], ],
"time": "2025-02-26T10:21:45+00:00" "time": "2025-02-28T13:11:00+00:00"
}, },
{ {
"name": "carbonphp/carbon-doctrine-types", "name": "carbonphp/carbon-doctrine-types",
@ -1203,16 +1203,16 @@
}, },
{ {
"name": "intervention/image", "name": "intervention/image",
"version": "3.11.1", "version": "3.11.2",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/Intervention/image.git", "url": "https://github.com/Intervention/image.git",
"reference": "0f87254688e480fbb521e2a1ac6c11c784ca41af" "reference": "ebbb711871fb261c064cf4c422f5f3c124fe1842"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/Intervention/image/zipball/0f87254688e480fbb521e2a1ac6c11c784ca41af", "url": "https://api.github.com/repos/Intervention/image/zipball/ebbb711871fb261c064cf4c422f5f3c124fe1842",
"reference": "0f87254688e480fbb521e2a1ac6c11c784ca41af", "reference": "ebbb711871fb261c064cf4c422f5f3c124fe1842",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -1223,7 +1223,7 @@
"require-dev": { "require-dev": {
"mockery/mockery": "^1.6", "mockery/mockery": "^1.6",
"phpstan/phpstan": "^2.1", "phpstan/phpstan": "^2.1",
"phpunit/phpunit": "^10.0 || ^11.0", "phpunit/phpunit": "^10.0 || ^11.0 || ^12.0",
"slevomat/coding-standard": "~8.0", "slevomat/coding-standard": "~8.0",
"squizlabs/php_codesniffer": "^3.8" "squizlabs/php_codesniffer": "^3.8"
}, },
@ -1259,7 +1259,7 @@
], ],
"support": { "support": {
"issues": "https://github.com/Intervention/image/issues", "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": [ "funding": [
{ {
@ -1275,7 +1275,7 @@
"type": "ko_fi" "type": "ko_fi"
} }
], ],
"time": "2025-02-01T07:28:26+00:00" "time": "2025-02-27T13:08:55+00:00"
}, },
{ {
"name": "laravel/framework", "name": "laravel/framework",
@ -2295,16 +2295,16 @@
}, },
{ {
"name": "livewire/flux", "name": "livewire/flux",
"version": "v2.0.3", "version": "v2.0.4",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/livewire/flux.git", "url": "https://github.com/livewire/flux.git",
"reference": "dec010f09419cd9d9930abc4b304802c379be57e" "reference": "6b0d59040715f072982bfc92fe71414b44d45a0c"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/livewire/flux/zipball/dec010f09419cd9d9930abc4b304802c379be57e", "url": "https://api.github.com/repos/livewire/flux/zipball/6b0d59040715f072982bfc92fe71414b44d45a0c",
"reference": "dec010f09419cd9d9930abc4b304802c379be57e", "reference": "6b0d59040715f072982bfc92fe71414b44d45a0c",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -2352,9 +2352,9 @@
], ],
"support": { "support": {
"issues": "https://github.com/livewire/flux/issues", "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", "name": "livewire/livewire",
@ -3618,16 +3618,16 @@
}, },
{ {
"name": "ramsey/collection", "name": "ramsey/collection",
"version": "2.0.0", "version": "2.1.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/ramsey/collection.git", "url": "https://github.com/ramsey/collection.git",
"reference": "a4b48764bfbb8f3a6a4d1aeb1a35bb5e9ecac4a5" "reference": "3c5990b8a5e0b79cd1cf11c2dc1229e58e93f109"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/ramsey/collection/zipball/a4b48764bfbb8f3a6a4d1aeb1a35bb5e9ecac4a5", "url": "https://api.github.com/repos/ramsey/collection/zipball/3c5990b8a5e0b79cd1cf11c2dc1229e58e93f109",
"reference": "a4b48764bfbb8f3a6a4d1aeb1a35bb5e9ecac4a5", "reference": "3c5990b8a5e0b79cd1cf11c2dc1229e58e93f109",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -3635,25 +3635,22 @@
}, },
"require-dev": { "require-dev": {
"captainhook/plugin-composer": "^5.3", "captainhook/plugin-composer": "^5.3",
"ergebnis/composer-normalize": "^2.28.3", "ergebnis/composer-normalize": "^2.45",
"fakerphp/faker": "^1.21", "fakerphp/faker": "^1.24",
"hamcrest/hamcrest-php": "^2.0", "hamcrest/hamcrest-php": "^2.0",
"jangregor/phpstan-prophecy": "^1.0", "jangregor/phpstan-prophecy": "^2.1",
"mockery/mockery": "^1.5", "mockery/mockery": "^1.6",
"php-parallel-lint/php-console-highlighter": "^1.0", "php-parallel-lint/php-console-highlighter": "^1.0",
"php-parallel-lint/php-parallel-lint": "^1.3", "php-parallel-lint/php-parallel-lint": "^1.4",
"phpcsstandards/phpcsutils": "^1.0.0-rc1", "phpspec/prophecy-phpunit": "^2.3",
"phpspec/prophecy-phpunit": "^2.0", "phpstan/extension-installer": "^1.4",
"phpstan/extension-installer": "^1.2", "phpstan/phpstan": "^2.1",
"phpstan/phpstan": "^1.9", "phpstan/phpstan-mockery": "^2.0",
"phpstan/phpstan-mockery": "^1.1", "phpstan/phpstan-phpunit": "^2.0",
"phpstan/phpstan-phpunit": "^1.3", "phpunit/phpunit": "^10.5",
"phpunit/phpunit": "^9.5", "ramsey/coding-standard": "^2.3",
"psalm/plugin-mockery": "^1.1", "ramsey/conventional-commits": "^1.6",
"psalm/plugin-phpunit": "^0.18.4", "roave/security-advisories": "dev-latest"
"ramsey/coding-standard": "^2.0.3",
"ramsey/conventional-commits": "^1.3",
"vimeo/psalm": "^5.4"
}, },
"type": "library", "type": "library",
"extra": { "extra": {
@ -3691,19 +3688,9 @@
], ],
"support": { "support": {
"issues": "https://github.com/ramsey/collection/issues", "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": [ "time": "2025-03-02T04:48:29+00:00"
{
"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"
}, },
{ {
"name": "ramsey/uuid", "name": "ramsey/uuid",
@ -3925,6 +3912,68 @@
], ],
"time": "2025-02-06T14:58:20+00:00" "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", "name": "spatie/temporary-directory",
"version": "2.3.0", "version": "2.3.0",

View file

@ -38,6 +38,7 @@ return [
'trmnl' => [ 'trmnl' => [
'proxy_base_url' => env('TRMNL_PROXY_BASE_URL', 'https://trmnl.app'), 'proxy_base_url' => env('TRMNL_PROXY_BASE_URL', 'https://trmnl.app'),
'proxy_refresh_minutes' => env('TRMNL_PROXY_REFRESH_MINUTES', 15), 'proxy_refresh_minutes' => env('TRMNL_PROXY_REFRESH_MINUTES', 15),
'override_orig_icon' => env('TRMNL_OVERRIDE_ORIG_ICON', false),
], ],
]; ];

View file

@ -14,12 +14,17 @@ class DeviceFactory extends Factory
public function definition(): array public function definition(): array
{ {
return [ return [
'name' => $this->faker->firstName().' TRMNL', 'name' => $this->faker->firstName().'\'s TRMNL',
'mac_address' => $this->faker->macAddress(), 'mac_address' => $this->faker->macAddress(),
'default_refresh_interval' => '900', 'default_refresh_interval' => '900',
'friendly_id' => Str::random(6), 'friendly_id' => Str::random(6),
'api_key' => 'tD-'.Str::random(19), 'api_key' => 'tD-'.Str::random(19),
'user_id' => 1, '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(), 'created_at' => Carbon::now(),
'updated_at' => Carbon::now(), 'updated_at' => Carbon::now(),
]; ];

View file

@ -29,6 +29,7 @@ class UserFactory extends Factory
'email_verified_at' => now(), 'email_verified_at' => now(),
'password' => static::$password ??= Hash::make('password'), 'password' => static::$password ??= Hash::make('password'),
'remember_token' => Str::random(10), 'remember_token' => Str::random(10),
'assign_new_devices' => false,
]; ];
} }

View file

@ -2,10 +2,12 @@
namespace Database\Seeders; namespace Database\Seeders;
use App\Models\Device;
use App\Models\User; use App\Models\User;
// use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder; use Illuminate\Database\Seeder;
// use Illuminate\Database\Console\Seeds\WithoutModelEvents;
class DatabaseSeeder extends Seeder class DatabaseSeeder extends Seeder
{ {
/** /**
@ -13,12 +15,14 @@ class DatabaseSeeder extends Seeder
*/ */
public function run(): void 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([ // Device::factory(5)->create();
'name' => 'Test User', }
'email' => 'admin@example.com',
'password' => bcrypt('admin@example.com'),
]);
} }
} }

340
package-lock.json generated
View file

@ -465,9 +465,9 @@
} }
}, },
"node_modules/@rollup/rollup-android-arm-eabi": { "node_modules/@rollup/rollup-android-arm-eabi": {
"version": "4.34.8", "version": "4.34.9",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.34.8.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.34.9.tgz",
"integrity": "sha512-q217OSE8DTp8AFHuNHXo0Y86e1wtlfVrXiAlwkIvGRQv9zbc6mE3sjIVfwI8sYUyNxwOg0j/Vm1RKM04JcWLJw==", "integrity": "sha512-qZdlImWXur0CFakn2BJ2znJOdqYZKiedEPEVNTBrpfPjc/YuTGcaYZcdmNFTkUj3DU0ZM/AElcM8Ybww3xVLzA==",
"cpu": [ "cpu": [
"arm" "arm"
], ],
@ -478,9 +478,9 @@
] ]
}, },
"node_modules/@rollup/rollup-android-arm64": { "node_modules/@rollup/rollup-android-arm64": {
"version": "4.34.8", "version": "4.34.9",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.34.8.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.34.9.tgz",
"integrity": "sha512-Gigjz7mNWaOL9wCggvoK3jEIUUbGul656opstjaUSGC3eT0BM7PofdAJaBfPFWWkXNVAXbaQtC99OCg4sJv70Q==", "integrity": "sha512-4KW7P53h6HtJf5Y608T1ISKvNIYLWRKMvfnG0c44M6In4DQVU58HZFEVhWINDZKp7FZps98G3gxwC1sb0wXUUg==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -491,9 +491,9 @@
] ]
}, },
"node_modules/@rollup/rollup-darwin-arm64": { "node_modules/@rollup/rollup-darwin-arm64": {
"version": "4.34.8", "version": "4.34.9",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.34.8.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.34.9.tgz",
"integrity": "sha512-02rVdZ5tgdUNRxIUrFdcMBZQoaPMrxtwSb+/hOfBdqkatYHR3lZ2A2EGyHq2sGOd0Owk80oV3snlDASC24He3Q==", "integrity": "sha512-0CY3/K54slrzLDjOA7TOjN1NuLKERBgk9nY5V34mhmuu673YNb+7ghaDUs6N0ujXR7fz5XaS5Aa6d2TNxZd0OQ==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -504,9 +504,9 @@
] ]
}, },
"node_modules/@rollup/rollup-darwin-x64": { "node_modules/@rollup/rollup-darwin-x64": {
"version": "4.34.8", "version": "4.34.9",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.34.8.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.34.9.tgz",
"integrity": "sha512-qIP/elwR/tq/dYRx3lgwK31jkZvMiD6qUtOycLhTzCvrjbZ3LjQnEM9rNhSGpbLXVJYQ3rq39A6Re0h9tU2ynw==", "integrity": "sha512-eOojSEAi/acnsJVYRxnMkPFqcxSMFfrw7r2iD9Q32SGkb/Q9FpUY1UlAu1DH9T7j++gZ0lHjnm4OyH2vCI7l7Q==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -517,9 +517,9 @@
] ]
}, },
"node_modules/@rollup/rollup-freebsd-arm64": { "node_modules/@rollup/rollup-freebsd-arm64": {
"version": "4.34.8", "version": "4.34.9",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.34.8.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.34.9.tgz",
"integrity": "sha512-IQNVXL9iY6NniYbTaOKdrlVP3XIqazBgJOVkddzJlqnCpRi/yAeSOa8PLcECFSQochzqApIOE1GHNu3pCz+BDA==", "integrity": "sha512-2lzjQPJbN5UnHm7bHIUKFMulGTQwdvOkouJDpPysJS+QFBGDJqcfh+CxxtG23Ik/9tEvnebQiylYoazFMAgrYw==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -530,9 +530,9 @@
] ]
}, },
"node_modules/@rollup/rollup-freebsd-x64": { "node_modules/@rollup/rollup-freebsd-x64": {
"version": "4.34.8", "version": "4.34.9",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.34.8.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.34.9.tgz",
"integrity": "sha512-TYXcHghgnCqYFiE3FT5QwXtOZqDj5GmaFNTNt3jNC+vh22dc/ukG2cG+pi75QO4kACohZzidsq7yKTKwq/Jq7Q==", "integrity": "sha512-SLl0hi2Ah2H7xQYd6Qaiu01kFPzQ+hqvdYSoOtHYg/zCIFs6t8sV95kaoqjzjFwuYQLtOI0RZre/Ke0nPaQV+g==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -543,9 +543,9 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-arm-gnueabihf": { "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
"version": "4.34.8", "version": "4.34.9",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.34.8.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.34.9.tgz",
"integrity": "sha512-A4iphFGNkWRd+5m3VIGuqHnG3MVnqKe7Al57u9mwgbyZ2/xF9Jio72MaY7xxh+Y87VAHmGQr73qoKL9HPbXj1g==", "integrity": "sha512-88I+D3TeKItrw+Y/2ud4Tw0+3CxQ2kLgu3QvrogZ0OfkmX/DEppehus7L3TS2Q4lpB+hYyxhkQiYPJ6Mf5/dPg==",
"cpu": [ "cpu": [
"arm" "arm"
], ],
@ -556,9 +556,9 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-arm-musleabihf": { "node_modules/@rollup/rollup-linux-arm-musleabihf": {
"version": "4.34.8", "version": "4.34.9",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.34.8.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.34.9.tgz",
"integrity": "sha512-S0lqKLfTm5u+QTxlFiAnb2J/2dgQqRy/XvziPtDd1rKZFXHTyYLoVL58M/XFwDI01AQCDIevGLbQrMAtdyanpA==", "integrity": "sha512-3qyfWljSFHi9zH0KgtEPG4cBXHDFhwD8kwg6xLfHQ0IWuH9crp005GfoUUh/6w9/FWGBwEHg3lxK1iHRN1MFlA==",
"cpu": [ "cpu": [
"arm" "arm"
], ],
@ -569,9 +569,9 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-arm64-gnu": { "node_modules/@rollup/rollup-linux-arm64-gnu": {
"version": "4.34.8", "version": "4.34.9",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.34.8.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.34.9.tgz",
"integrity": "sha512-jpz9YOuPiSkL4G4pqKrus0pn9aYwpImGkosRKwNi+sJSkz+WU3anZe6hi73StLOQdfXYXC7hUfsQlTnjMd3s1A==", "integrity": "sha512-6TZjPHjKZUQKmVKMUowF3ewHxctrRR09eYyvT5eFv8w/fXarEra83A2mHTVJLA5xU91aCNOUnM+DWFMSbQ0Nxw==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -582,9 +582,9 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-arm64-musl": { "node_modules/@rollup/rollup-linux-arm64-musl": {
"version": "4.34.8", "version": "4.34.9",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.34.8.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.34.9.tgz",
"integrity": "sha512-KdSfaROOUJXgTVxJNAZ3KwkRc5nggDk+06P6lgi1HLv1hskgvxHUKZ4xtwHkVYJ1Rep4GNo+uEfycCRRxht7+Q==", "integrity": "sha512-LD2fytxZJZ6xzOKnMbIpgzFOuIKlxVOpiMAXawsAZ2mHBPEYOnLRK5TTEsID6z4eM23DuO88X0Tq1mErHMVq0A==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -595,9 +595,9 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-loongarch64-gnu": { "node_modules/@rollup/rollup-linux-loongarch64-gnu": {
"version": "4.34.8", "version": "4.34.9",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.34.8.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.34.9.tgz",
"integrity": "sha512-NyF4gcxwkMFRjgXBM6g2lkT58OWztZvw5KkV2K0qqSnUEqCVcqdh2jN4gQrTn/YUpAcNKyFHfoOZEer9nwo6uQ==", "integrity": "sha512-dRAgTfDsn0TE0HI6cmo13hemKpVHOEyeciGtvlBTkpx/F65kTvShtY/EVyZEIfxFkV5JJTuQ9tP5HGBS0hfxIg==",
"cpu": [ "cpu": [
"loong64" "loong64"
], ],
@ -608,9 +608,9 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-powerpc64le-gnu": { "node_modules/@rollup/rollup-linux-powerpc64le-gnu": {
"version": "4.34.8", "version": "4.34.9",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.34.8.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.34.9.tgz",
"integrity": "sha512-LMJc999GkhGvktHU85zNTDImZVUCJ1z/MbAJTnviiWmmjyckP5aQsHtcujMjpNdMZPT2rQEDBlJfubhs3jsMfw==", "integrity": "sha512-PHcNOAEhkoMSQtMf+rJofwisZqaU8iQ8EaSps58f5HYll9EAY5BSErCZ8qBDMVbq88h4UxaNPlbrKqfWP8RfJA==",
"cpu": [ "cpu": [
"ppc64" "ppc64"
], ],
@ -621,9 +621,9 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-riscv64-gnu": { "node_modules/@rollup/rollup-linux-riscv64-gnu": {
"version": "4.34.8", "version": "4.34.9",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.34.8.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.34.9.tgz",
"integrity": "sha512-xAQCAHPj8nJq1PI3z8CIZzXuXCstquz7cIOL73HHdXiRcKk8Ywwqtx2wrIy23EcTn4aZ2fLJNBB8d0tQENPCmw==", "integrity": "sha512-Z2i0Uy5G96KBYKjeQFKbbsB54xFOL5/y1P5wNBsbXB8yE+At3oh0DVMjQVzCJRJSfReiB2tX8T6HUFZ2k8iaKg==",
"cpu": [ "cpu": [
"riscv64" "riscv64"
], ],
@ -634,9 +634,9 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-s390x-gnu": { "node_modules/@rollup/rollup-linux-s390x-gnu": {
"version": "4.34.8", "version": "4.34.9",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.34.8.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.34.9.tgz",
"integrity": "sha512-DdePVk1NDEuc3fOe3dPPTb+rjMtuFw89gw6gVWxQFAuEqqSdDKnrwzZHrUYdac7A7dXl9Q2Vflxpme15gUWQFA==", "integrity": "sha512-U+5SwTMoeYXoDzJX5dhDTxRltSrIax8KWwfaaYcynuJw8mT33W7oOgz0a+AaXtGuvhzTr2tVKh5UO8GVANTxyQ==",
"cpu": [ "cpu": [
"s390x" "s390x"
], ],
@ -660,9 +660,9 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-x64-musl": { "node_modules/@rollup/rollup-linux-x64-musl": {
"version": "4.34.8", "version": "4.34.9",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.34.8.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.34.9.tgz",
"integrity": "sha512-SCXcP0ZpGFIe7Ge+McxY5zKxiEI5ra+GT3QRxL0pMMtxPfpyLAKleZODi1zdRHkz5/BhueUrYtYVgubqe9JBNQ==", "integrity": "sha512-cYRpV4650z2I3/s6+5/LONkjIz8MBeqrk+vPXV10ORBnshpn8S32bPqQ2Utv39jCiDcO2eJTuSlPXpnvmaIgRA==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -673,9 +673,9 @@
] ]
}, },
"node_modules/@rollup/rollup-win32-arm64-msvc": { "node_modules/@rollup/rollup-win32-arm64-msvc": {
"version": "4.34.8", "version": "4.34.9",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.34.8.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.34.9.tgz",
"integrity": "sha512-YHYsgzZgFJzTRbth4h7Or0m5O74Yda+hLin0irAIobkLQFRQd1qWmnoVfwmKm9TXIZVAD0nZ+GEb2ICicLyCnQ==", "integrity": "sha512-z4mQK9dAN6byRA/vsSgQiPeuO63wdiDxZ9yg9iyX2QTzKuQM7T4xlBoeUP/J8uiFkqxkcWndWi+W7bXdPbt27Q==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -686,9 +686,9 @@
] ]
}, },
"node_modules/@rollup/rollup-win32-ia32-msvc": { "node_modules/@rollup/rollup-win32-ia32-msvc": {
"version": "4.34.8", "version": "4.34.9",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.34.8.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.34.9.tgz",
"integrity": "sha512-r3NRQrXkHr4uWy5TOjTpTYojR9XmF0j/RYgKCef+Ag46FWUTltm5ziticv8LdNsDMehjJ543x/+TJAek/xBA2w==", "integrity": "sha512-KB48mPtaoHy1AwDNkAJfHXvHp24H0ryZog28spEs0V48l3H1fr4i37tiyHsgKZJnCmvxsbATdZGBpbmxTE3a9w==",
"cpu": [ "cpu": [
"ia32" "ia32"
], ],
@ -699,9 +699,9 @@
] ]
}, },
"node_modules/@rollup/rollup-win32-x64-msvc": { "node_modules/@rollup/rollup-win32-x64-msvc": {
"version": "4.34.8", "version": "4.34.9",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.34.8.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.34.9.tgz",
"integrity": "sha512-U0FaE5O1BCpZSeE6gBl3c5ObhePQSfk9vDRToMmTkbhCOgW4jqvtS5LGyQ76L1fH8sM0keRp4uDTsbjiUyjk0g==", "integrity": "sha512-AyleYRPU7+rgkMWbEh71fQlrzRfeP6SyMnRf9XX4fCdDPAJumdSBqYEcWPMzVQ4ScAl7E4oFfK0GUVn77xSwbw==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -712,42 +712,42 @@
] ]
}, },
"node_modules/@tailwindcss/node": { "node_modules/@tailwindcss/node": {
"version": "4.0.8", "version": "4.0.9",
"resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.0.8.tgz", "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.0.9.tgz",
"integrity": "sha512-FKArQpbrbwv08TNT0k7ejYXpF+R8knZFAatNc0acOxbgeqLzwb86r+P3LGOjIeI3Idqe9CVkZrh4GlsJLJKkkw==", "integrity": "sha512-tOJvdI7XfJbARYhxX+0RArAhmuDcczTC46DGCEziqxzzbIaPnfYaIyRT31n4u8lROrsO7Q6u/K9bmQHL2uL1bQ==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"enhanced-resolve": "^5.18.1", "enhanced-resolve": "^5.18.1",
"jiti": "^2.4.2", "jiti": "^2.4.2",
"tailwindcss": "4.0.8" "tailwindcss": "4.0.9"
} }
}, },
"node_modules/@tailwindcss/oxide": { "node_modules/@tailwindcss/oxide": {
"version": "4.0.8", "version": "4.0.9",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.0.8.tgz", "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.0.9.tgz",
"integrity": "sha512-KfMcuAu/Iw+DcV1e8twrFyr2yN8/ZDC/odIGta4wuuJOGkrkHZbvJvRNIbQNhGh7erZTYV6Ie0IeD6WC9Y8Hcw==", "integrity": "sha512-eLizHmXFqHswJONwfqi/WZjtmWZpIalpvMlNhTM99/bkHtUs6IqgI1XQ0/W5eO2HiRQcIlXUogI2ycvKhVLNcA==",
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">= 10" "node": ">= 10"
}, },
"optionalDependencies": { "optionalDependencies": {
"@tailwindcss/oxide-android-arm64": "4.0.8", "@tailwindcss/oxide-android-arm64": "4.0.9",
"@tailwindcss/oxide-darwin-arm64": "4.0.8", "@tailwindcss/oxide-darwin-arm64": "4.0.9",
"@tailwindcss/oxide-darwin-x64": "4.0.8", "@tailwindcss/oxide-darwin-x64": "4.0.9",
"@tailwindcss/oxide-freebsd-x64": "4.0.8", "@tailwindcss/oxide-freebsd-x64": "4.0.9",
"@tailwindcss/oxide-linux-arm-gnueabihf": "4.0.8", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.0.9",
"@tailwindcss/oxide-linux-arm64-gnu": "4.0.8", "@tailwindcss/oxide-linux-arm64-gnu": "4.0.9",
"@tailwindcss/oxide-linux-arm64-musl": "4.0.8", "@tailwindcss/oxide-linux-arm64-musl": "4.0.9",
"@tailwindcss/oxide-linux-x64-gnu": "4.0.8", "@tailwindcss/oxide-linux-x64-gnu": "4.0.9",
"@tailwindcss/oxide-linux-x64-musl": "4.0.8", "@tailwindcss/oxide-linux-x64-musl": "4.0.9",
"@tailwindcss/oxide-win32-arm64-msvc": "4.0.8", "@tailwindcss/oxide-win32-arm64-msvc": "4.0.9",
"@tailwindcss/oxide-win32-x64-msvc": "4.0.8" "@tailwindcss/oxide-win32-x64-msvc": "4.0.9"
} }
}, },
"node_modules/@tailwindcss/oxide-android-arm64": { "node_modules/@tailwindcss/oxide-android-arm64": {
"version": "4.0.8", "version": "4.0.9",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.0.8.tgz", "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.0.9.tgz",
"integrity": "sha512-We7K79+Sm4mwJHk26Yzu/GAj7C7myemm7PeXvpgMxyxO70SSFSL3uCcqFbz9JA5M5UPkrl7N9fkBe/Y0iazqpA==", "integrity": "sha512-YBgy6+2flE/8dbtrdotVInhMVIxnHJPbAwa7U1gX4l2ThUIaPUp18LjB9wEH8wAGMBZUb//SzLtdXXNBHPUl6Q==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -761,9 +761,9 @@
} }
}, },
"node_modules/@tailwindcss/oxide-darwin-arm64": { "node_modules/@tailwindcss/oxide-darwin-arm64": {
"version": "4.0.8", "version": "4.0.9",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.0.8.tgz", "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.0.9.tgz",
"integrity": "sha512-Lv9Isi2EwkCTG1sRHNDi0uRNN1UGFdEThUAGFrydRmQZnraGLMjN8gahzg2FFnOizDl7LB2TykLUuiw833DSNg==", "integrity": "sha512-pWdl4J2dIHXALgy2jVkwKBmtEb73kqIfMpYmcgESr7oPQ+lbcQ4+tlPeVXaSAmang+vglAfFpXQCOvs/aGSqlw==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -777,9 +777,9 @@
} }
}, },
"node_modules/@tailwindcss/oxide-darwin-x64": { "node_modules/@tailwindcss/oxide-darwin-x64": {
"version": "4.0.8", "version": "4.0.9",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.0.8.tgz", "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.0.9.tgz",
"integrity": "sha512-fWfywfYIlSWtKoqWTjukTHLWV3ARaBRjXCC2Eo0l6KVpaqGY4c2y8snUjp1xpxUtpqwMvCvFWFaleMoz1Vhzlw==", "integrity": "sha512-4Dq3lKp0/C7vrRSkNPtBGVebEyWt9QPPlQctxJ0H3MDyiQYvzVYf8jKow7h5QkWNe8hbatEqljMj/Y0M+ERYJg==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -793,9 +793,9 @@
} }
}, },
"node_modules/@tailwindcss/oxide-freebsd-x64": { "node_modules/@tailwindcss/oxide-freebsd-x64": {
"version": "4.0.8", "version": "4.0.9",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.0.8.tgz", "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.0.9.tgz",
"integrity": "sha512-SO+dyvjJV9G94bnmq2288Ke0BIdvrbSbvtPLaQdqjqHR83v5L2fWADyFO+1oecHo9Owsk8MxcXh1agGVPIKIqw==", "integrity": "sha512-k7U1RwRODta8x0uealtVt3RoWAWqA+D5FAOsvVGpYoI6ObgmnzqWW6pnVwz70tL8UZ/QXjeMyiICXyjzB6OGtQ==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -809,9 +809,9 @@
} }
}, },
"node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": {
"version": "4.0.8", "version": "4.0.9",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.0.8.tgz", "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.0.9.tgz",
"integrity": "sha512-ZSHggWiEblQNV69V0qUK5vuAtHP+I+S2eGrKGJ5lPgwgJeAd6GjLsVBN+Mqn2SPVfYM3BOpS9jX/zVg9RWQVDQ==", "integrity": "sha512-NDDjVweHz2zo4j+oS8y3KwKL5wGCZoXGA9ruJM982uVJLdsF8/1AeKvUwKRlMBpxHt1EdWJSAh8a0Mfhl28GlQ==",
"cpu": [ "cpu": [
"arm" "arm"
], ],
@ -825,9 +825,9 @@
} }
}, },
"node_modules/@tailwindcss/oxide-linux-arm64-gnu": { "node_modules/@tailwindcss/oxide-linux-arm64-gnu": {
"version": "4.0.8", "version": "4.0.9",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.0.8.tgz", "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.0.9.tgz",
"integrity": "sha512-xWpr6M0OZLDNsr7+bQz+3X7zcnDJZJ1N9gtBWCtfhkEtDjjxYEp+Lr5L5nc/yXlL4MyCHnn0uonGVXy3fhxaVA==", "integrity": "sha512-jk90UZ0jzJl3Dy1BhuFfRZ2KP9wVKMXPjmCtY4U6fF2LvrjP5gWFJj5VHzfzHonJexjrGe1lMzgtjriuZkxagg==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -841,9 +841,9 @@
} }
}, },
"node_modules/@tailwindcss/oxide-linux-arm64-musl": { "node_modules/@tailwindcss/oxide-linux-arm64-musl": {
"version": "4.0.8", "version": "4.0.9",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.0.8.tgz", "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.0.9.tgz",
"integrity": "sha512-5tz2IL7LN58ssGEq7h/staD7pu/izF/KeMWdlJ86WDe2Ah46LF3ET6ZGKTr5eZMrnEA0M9cVFuSPprKRHNgjeg==", "integrity": "sha512-3eMjyTC6HBxh9nRgOHzrc96PYh1/jWOwHZ3Kk0JN0Kl25BJ80Lj9HEvvwVDNTgPg154LdICwuFLuhfgH9DULmg==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -857,9 +857,9 @@
} }
}, },
"node_modules/@tailwindcss/oxide-linux-x64-gnu": { "node_modules/@tailwindcss/oxide-linux-x64-gnu": {
"version": "4.0.8", "version": "4.0.9",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.0.8.tgz", "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.0.9.tgz",
"integrity": "sha512-KSzMkhyrxAQyY2o194NKVKU9j/c+NFSoMvnHWFaNHKi3P1lb+Vq1UC19tLHrmxSkKapcMMu69D7+G1+FVGNDXQ==", "integrity": "sha512-v0D8WqI/c3WpWH1kq/HP0J899ATLdGZmENa2/emmNjubT0sWtEke9W9+wXeEoACuGAhF9i3PO5MeyditpDCiWQ==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -873,9 +873,9 @@
} }
}, },
"node_modules/@tailwindcss/oxide-linux-x64-musl": { "node_modules/@tailwindcss/oxide-linux-x64-musl": {
"version": "4.0.8", "version": "4.0.9",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.0.8.tgz", "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.0.9.tgz",
"integrity": "sha512-yFYKG5UtHTRimjtqxUWXBgI4Tc6NJe3USjRIVdlTczpLRxq/SFwgzGl5JbatCxgSRDPBFwRrNPxq+ukfQFGdrw==", "integrity": "sha512-Kvp0TCkfeXyeehqLJr7otsc4hd/BUPfcIGrQiwsTVCfaMfjQZCG7DjI+9/QqPZha8YapLA9UoIcUILRYO7NE1Q==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -889,9 +889,9 @@
} }
}, },
"node_modules/@tailwindcss/oxide-win32-arm64-msvc": { "node_modules/@tailwindcss/oxide-win32-arm64-msvc": {
"version": "4.0.8", "version": "4.0.9",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.0.8.tgz", "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.0.9.tgz",
"integrity": "sha512-tndGujmCSba85cRCnQzXgpA2jx5gXimyspsUYae5jlPyLRG0RjXbDshFKOheVXU4TLflo7FSG8EHCBJ0EHTKdQ==", "integrity": "sha512-m3+60T/7YvWekajNq/eexjhV8z10rswcz4BC9bioJ7YaN+7K8W2AmLmG0B79H14m6UHE571qB0XsPus4n0QVgQ==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -905,9 +905,9 @@
} }
}, },
"node_modules/@tailwindcss/oxide-win32-x64-msvc": { "node_modules/@tailwindcss/oxide-win32-x64-msvc": {
"version": "4.0.8", "version": "4.0.9",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.0.8.tgz", "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.0.9.tgz",
"integrity": "sha512-T77jroAc0p4EHVVgTUiNeFn6Nj3jtD3IeNId2X+0k+N1XxfNipy81BEkYErpKLiOkNhpNFjPee8/ZVas29b2OQ==", "integrity": "sha512-dpc05mSlqkwVNOUjGu/ZXd5U1XNch1kHFJ4/cHkZFvaW1RzbHmRt24gvM8/HC6IirMxNarzVw4IXVtvrOoZtxA==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -921,15 +921,15 @@
} }
}, },
"node_modules/@tailwindcss/vite": { "node_modules/@tailwindcss/vite": {
"version": "4.0.8", "version": "4.0.9",
"resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.0.8.tgz", "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.0.9.tgz",
"integrity": "sha512-+SAq44yLzYlzyrb7QTcFCdU8Xa7FOA0jp+Xby7fPMUie+MY9HhJysM7Vp+vL8qIp8ceQJfLD+FjgJuJ4lL6nyg==", "integrity": "sha512-BIKJO+hwdIsN7V6I7SziMZIVHWWMsV/uCQKYEbeiGRDRld+TkqyRRl9+dQ0MCXbhcVr+D9T/qX2E84kT7V281g==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@tailwindcss/node": "4.0.8", "@tailwindcss/node": "4.0.9",
"@tailwindcss/oxide": "4.0.8", "@tailwindcss/oxide": "4.0.9",
"lightningcss": "^1.29.1", "lightningcss": "^1.29.1",
"tailwindcss": "4.0.8" "tailwindcss": "4.0.9"
}, },
"peerDependencies": { "peerDependencies": {
"vite": "^5.2.0 || ^6" "vite": "^5.2.0 || ^6"
@ -948,9 +948,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/node": { "node_modules/@types/node": {
"version": "22.13.5", "version": "22.13.9",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.5.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.9.tgz",
"integrity": "sha512-+lTU0PxZXn0Dr1NBtC7Y8cR21AJr87dLLU953CWA6pMxxv/UDc7jYAY90upcrie1nRcD6XNG5HOYEDtgW5TxAg==", "integrity": "sha512-acBjXdRJ3A6Pb3tqnw9HZmyR3Fiol3aGxRCK1x3d+6CDAMjl7I649wpSd+yNURCjbOUGu9tqtLKnTGxmK6CyGw==",
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"dependencies": { "dependencies": {
@ -1062,9 +1062,9 @@
} }
}, },
"node_modules/axios": { "node_modules/axios": {
"version": "1.7.9", "version": "1.8.1",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.7.9.tgz", "resolved": "https://registry.npmjs.org/axios/-/axios-1.8.1.tgz",
"integrity": "sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw==", "integrity": "sha512-NN+fvwH/kV01dYUQ3PTOZns4LWtWhOFCAhQ/pHb88WQ1hNe5V/dvFwc4VJcDL11LT9xSX0QtsR8sWUuyOuOq7g==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"follow-redirects": "^1.15.6", "follow-redirects": "^1.15.6",
@ -1101,13 +1101,13 @@
} }
}, },
"node_modules/bare-os": { "node_modules/bare-os": {
"version": "3.4.0", "version": "3.5.1",
"resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.4.0.tgz", "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.5.1.tgz",
"integrity": "sha512-9Ous7UlnKbe3fMi7Y+qh0DwAup6A1JkYgPnjvMDNOlmnxNRQvQ/7Nst+OnUQKzk0iAT0m9BisbDVp9gCv8+ETA==", "integrity": "sha512-LvfVNDcWLw2AnIw5f2mWUgumW3I3N/WYGiWeimhQC1Ybt71n2FjlS9GJKeCnFeg1MKZHxzIFmpFnBXDI+sBeFg==",
"license": "Apache-2.0", "license": "Apache-2.0",
"optional": true, "optional": true,
"engines": { "engines": {
"bare": ">=1.6.0" "bare": ">=1.14.0"
} }
}, },
"node_modules/bare-path": { "node_modules/bare-path": {
@ -1215,9 +1215,9 @@
} }
}, },
"node_modules/caniuse-lite": { "node_modules/caniuse-lite": {
"version": "1.0.30001700", "version": "1.0.30001702",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001700.tgz", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001702.tgz",
"integrity": "sha512-2S6XIXwaE7K7erT8dY+kLQcpa5ms63XlRkMkReXjle+kf6c5g38vyMl+Z5y8dSxOFDhcFe+nxnn261PLxBSQsQ==", "integrity": "sha512-LoPe/D7zioC0REI5W73PeR1e1MLCipRGq/VkovJnd6Df+QVqT+vT33OXCp8QUd7kA7RZrHWxb1B36OQKI/0gOA==",
"funding": [ "funding": [
{ {
"type": "opencollective", "type": "opencollective",
@ -1263,9 +1263,9 @@
} }
}, },
"node_modules/chromium-bidi": { "node_modules/chromium-bidi": {
"version": "2.0.0", "version": "2.1.2",
"resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-2.0.0.tgz", "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-2.1.2.tgz",
"integrity": "sha512-8VmyVj0ewSY4pstZV0Y3rCUUwpomam8uWgHZf1XavRxJEP4vU9/dcpNuoyB+u4AQxPo96CASXz5CHPvdH+dSeQ==", "integrity": "sha512-vtRWBK2uImo5/W2oG6/cDkkHSm+2t6VHgnj+Rcwhb0pP74OoUb4GipyRX/T/y39gYQPhioP0DPShn+A7P6CHNw==",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"mitt": "^3.0.1", "mitt": "^3.0.1",
@ -1452,9 +1452,9 @@
} }
}, },
"node_modules/electron-to-chromium": { "node_modules/electron-to-chromium": {
"version": "1.5.104", "version": "1.5.112",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.104.tgz", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.112.tgz",
"integrity": "sha512-Us9M2L4cO/zMBqVkJtnj353nQhMju9slHm62NprKTmdF3HH8wYOtNvDFq/JB2+ZRoGLzdvYDiATlMHs98XBM1g==", "integrity": "sha512-oen93kVyqSb3l+ziUgzIOlWt/oOuy4zRmpwestMn4rhFWAoFJeFuCVte9F2fASjeZZo7l/Cif9TiyrdW4CwEMA==",
"license": "ISC" "license": "ISC"
}, },
"node_modules/emoji-regex": { "node_modules/emoji-regex": {
@ -2531,17 +2531,17 @@
} }
}, },
"node_modules/puppeteer": { "node_modules/puppeteer": {
"version": "24.3.0", "version": "24.3.1",
"resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-24.3.0.tgz", "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-24.3.1.tgz",
"integrity": "sha512-wYEx+NnEM1T6ncHB+IsTovUgx+JlZ0pv0sRGTb8IzoTeOILvyUcdU2h34bYEQ1iG5maz1VQA5eI4kzIyAVh90A==", "integrity": "sha512-k0OJ7itRwkr06owp0CP3f/PsRD7Pdw4DjoCUZvjGr+aNgS1z6n/61VajIp0uBjl+V5XAQO1v/3k9bzeZLWs9OQ==",
"hasInstallScript": true, "hasInstallScript": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@puppeteer/browsers": "2.7.1", "@puppeteer/browsers": "2.7.1",
"chromium-bidi": "2.0.0", "chromium-bidi": "2.1.2",
"cosmiconfig": "^9.0.0", "cosmiconfig": "^9.0.0",
"devtools-protocol": "0.0.1402036", "devtools-protocol": "0.0.1402036",
"puppeteer-core": "24.3.0", "puppeteer-core": "24.3.1",
"typed-query-selector": "^2.12.0" "typed-query-selector": "^2.12.0"
}, },
"bin": { "bin": {
@ -2552,13 +2552,13 @@
} }
}, },
"node_modules/puppeteer-core": { "node_modules/puppeteer-core": {
"version": "24.3.0", "version": "24.3.1",
"resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-24.3.0.tgz", "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-24.3.1.tgz",
"integrity": "sha512-x8kQRP/xxtiFav6wWuLzrctO0HWRpSQy+JjaHbqIl+d5U2lmRh2pY9vh5AzDFN0EtOXW2pzngi9RrryY1vZGig==", "integrity": "sha512-585ccfcTav4KmlSmYbwwOSeC8VdutQHn2Fuk0id/y/9OoeO7Gg5PK1aUGdZjEmos0TAq+pCpChqFurFbpNd3wA==",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@puppeteer/browsers": "2.7.1", "@puppeteer/browsers": "2.7.1",
"chromium-bidi": "2.0.0", "chromium-bidi": "2.1.2",
"debug": "^4.4.0", "debug": "^4.4.0",
"devtools-protocol": "0.0.1402036", "devtools-protocol": "0.0.1402036",
"typed-query-selector": "^2.12.0", "typed-query-selector": "^2.12.0",
@ -2587,9 +2587,9 @@
} }
}, },
"node_modules/rollup": { "node_modules/rollup": {
"version": "4.34.8", "version": "4.34.9",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.34.8.tgz", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.34.9.tgz",
"integrity": "sha512-489gTVMzAYdiZHFVA/ig/iYFllCcWFHMvUHI1rpFmkoUtRlQxqh6/yiNqnYibjMZ2b/+FUQwldG+aLsEt6bglQ==", "integrity": "sha512-nF5XYqWWp9hx/LrpC8sZvvvmq0TeTjQgaZHYmAgwysT9nh8sWnZhBnM8ZyVbbJFIQBLwHDNoMqsBZBbUo4U8sQ==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@types/estree": "1.0.6" "@types/estree": "1.0.6"
@ -2602,32 +2602,32 @@
"npm": ">=8.0.0" "npm": ">=8.0.0"
}, },
"optionalDependencies": { "optionalDependencies": {
"@rollup/rollup-android-arm-eabi": "4.34.8", "@rollup/rollup-android-arm-eabi": "4.34.9",
"@rollup/rollup-android-arm64": "4.34.8", "@rollup/rollup-android-arm64": "4.34.9",
"@rollup/rollup-darwin-arm64": "4.34.8", "@rollup/rollup-darwin-arm64": "4.34.9",
"@rollup/rollup-darwin-x64": "4.34.8", "@rollup/rollup-darwin-x64": "4.34.9",
"@rollup/rollup-freebsd-arm64": "4.34.8", "@rollup/rollup-freebsd-arm64": "4.34.9",
"@rollup/rollup-freebsd-x64": "4.34.8", "@rollup/rollup-freebsd-x64": "4.34.9",
"@rollup/rollup-linux-arm-gnueabihf": "4.34.8", "@rollup/rollup-linux-arm-gnueabihf": "4.34.9",
"@rollup/rollup-linux-arm-musleabihf": "4.34.8", "@rollup/rollup-linux-arm-musleabihf": "4.34.9",
"@rollup/rollup-linux-arm64-gnu": "4.34.8", "@rollup/rollup-linux-arm64-gnu": "4.34.9",
"@rollup/rollup-linux-arm64-musl": "4.34.8", "@rollup/rollup-linux-arm64-musl": "4.34.9",
"@rollup/rollup-linux-loongarch64-gnu": "4.34.8", "@rollup/rollup-linux-loongarch64-gnu": "4.34.9",
"@rollup/rollup-linux-powerpc64le-gnu": "4.34.8", "@rollup/rollup-linux-powerpc64le-gnu": "4.34.9",
"@rollup/rollup-linux-riscv64-gnu": "4.34.8", "@rollup/rollup-linux-riscv64-gnu": "4.34.9",
"@rollup/rollup-linux-s390x-gnu": "4.34.8", "@rollup/rollup-linux-s390x-gnu": "4.34.9",
"@rollup/rollup-linux-x64-gnu": "4.34.8", "@rollup/rollup-linux-x64-gnu": "4.34.9",
"@rollup/rollup-linux-x64-musl": "4.34.8", "@rollup/rollup-linux-x64-musl": "4.34.9",
"@rollup/rollup-win32-arm64-msvc": "4.34.8", "@rollup/rollup-win32-arm64-msvc": "4.34.9",
"@rollup/rollup-win32-ia32-msvc": "4.34.8", "@rollup/rollup-win32-ia32-msvc": "4.34.9",
"@rollup/rollup-win32-x64-msvc": "4.34.8", "@rollup/rollup-win32-x64-msvc": "4.34.9",
"fsevents": "~2.3.2" "fsevents": "~2.3.2"
} }
}, },
"node_modules/rollup/node_modules/@rollup/rollup-linux-x64-gnu": { "node_modules/rollup/node_modules/@rollup/rollup-linux-x64-gnu": {
"version": "4.34.8", "version": "4.34.9",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.34.8.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.34.9.tgz",
"integrity": "sha512-8y7ED8gjxITUltTUEJLQdgpbPh1sUQ0kMTmufRF/Ns5tI9TNMNlhWtmPKKHCU0SilX+3MJkZ0zERYYGIVBYHIA==", "integrity": "sha512-FwBHNSOjUTQLP4MG7y6rR6qbGw4MFeQnIBrMe161QGaQoBQLqSUEKlHIiVgF3g/mb3lxlxzJOpIBhaP+C+KP2A==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -2788,9 +2788,9 @@
} }
}, },
"node_modules/tailwindcss": { "node_modules/tailwindcss": {
"version": "4.0.8", "version": "4.0.9",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.0.8.tgz", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.0.9.tgz",
"integrity": "sha512-Me7N5CKR+D2A1xdWA5t5+kjjT7bwnxZOE6/yDI/ixJdJokszsn2n++mdU5yJwrsTpqFX2B9ZNMBJDwcqk9C9lw==", "integrity": "sha512-12laZu+fv1ONDRoNR9ipTOpUD7RN9essRVkX36sjxuRUInpN7hIiHN4lBd/SIFjbISvnXzp8h/hXzmU8SQQYhw==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/tapable": { "node_modules/tapable": {
@ -2865,9 +2865,9 @@
"optional": true "optional": true
}, },
"node_modules/update-browserslist-db": { "node_modules/update-browserslist-db": {
"version": "1.1.2", "version": "1.1.3",
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.2.tgz", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz",
"integrity": "sha512-PPypAm5qvlD7XMZC3BujecnaOxwhrtoFR+Dqkk5Aa/6DssiH0ibKoketaj9w8LP7Bont1rYeoV5plxD7RTEPRg==", "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==",
"funding": [ "funding": [
{ {
"type": "opencollective", "type": "opencollective",

View file

@ -1,16 +1,33 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 30 30" {{ $attributes }}> @if(config('services.trmnl.override_orig_icon'))
<g clip-path="url(#clip0_870_1047)"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 30 30" {{ $attributes }}>
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.02194 2.11328L15.8863 5.07445L14.4259 8.98481L6.56152 6.02364L8.02194 2.11328Z" fill="currentColor"/> <g clip-path="url(#clip0_870_1047)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M20.6315 1.14647L23.2291 9.16642L19.2738 10.458L16.6761 2.43807L20.6315 1.14647Z" fill="currentColor"/> <path fill-rule="evenodd" clip-rule="evenodd"
<path fill-rule="evenodd" clip-rule="evenodd" d="M29.2441 10.4452L24.619 17.4848L21.1471 15.185L25.7723 8.14549L29.2441 10.4452Z" fill="currentColor"/> d="M8.02194 2.11328L15.8863 5.07445L14.4259 8.98481L6.56152 6.02364L8.02194 2.11328Z"
<path fill-rule="evenodd" clip-rule="evenodd" d="M27.3739 23.0034L19.0088 23.7616L18.6349 19.6023L27 18.8441L27.3739 23.0034Z" fill="currentColor"/> fill="currentColor"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M16.4296 29.3638L10.6236 23.2697L13.6292 20.3828L19.4351 26.4769L16.4296 29.3638Z" fill="currentColor"/> <path fill-rule="evenodd" clip-rule="evenodd"
<path fill-rule="evenodd" clip-rule="evenodd" d="M4.65232 24.7423L5.77753 16.3849L9.89932 16.9443L8.77411 25.3017L4.65232 24.7423Z" fill="currentColor"/> d="M20.6315 1.14647L23.2291 9.16642L19.2738 10.458L16.6761 2.43807L20.6315 1.14647Z"
<path fill-rule="evenodd" clip-rule="evenodd" d="M0.910558 12.6074L8.11961 8.28L10.2539 11.8645L3.04481 16.192L0.910558 12.6074Z" fill="currentColor"/> fill="currentColor"/>
</g> <path fill-rule="evenodd" clip-rule="evenodd"
<defs> d="M29.2441 10.4452L24.619 17.4848L21.1471 15.185L25.7723 8.14549L29.2441 10.4452Z"
<clipPath id="clip0_870_1047"> fill="currentColor"/>
<rect width="30" height="30" fill="currentColor"/> <path fill-rule="evenodd" clip-rule="evenodd"
</clipPath> d="M27.3739 23.0034L19.0088 23.7616L18.6349 19.6023L27 18.8441L27.3739 23.0034Z" fill="currentColor"/>
</defs> <path fill-rule="evenodd" clip-rule="evenodd"
</svg> d="M16.4296 29.3638L10.6236 23.2697L13.6292 20.3828L19.4351 26.4769L16.4296 29.3638Z"
fill="currentColor"/>
<path fill-rule="evenodd" clip-rule="evenodd"
d="M4.65232 24.7423L5.77753 16.3849L9.89932 16.9443L8.77411 25.3017L4.65232 24.7423Z"
fill="currentColor"/>
<path fill-rule="evenodd" clip-rule="evenodd"
d="M0.910558 12.6074L8.11961 8.28L10.2539 11.8645L3.04481 16.192L0.910558 12.6074Z"
fill="currentColor"/>
</g>
<defs>
<clipPath id="clip0_870_1047">
<rect width="30" height="30" fill="currentColor"/>
</clipPath>
</defs>
</svg>
@else
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-trello"><rect width="18" height="18" x="3" y="3" rx="2" ry="2"/><rect width="3" height="9" x="7" y="7"/><rect width="3" height="5" x="14" y="7"/></svg>
@endif

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

Before After
Before After

View file

@ -11,42 +11,44 @@ new class extends Component {
?> ?>
<div> <div>
<div class="flex w-full max-w-3xl flex-col gap-6"> <div class="bg-muted flex flex-col items-center justify-center gap-6 p-6 md:p-10">
@if($devices->isEmpty()) <div class="flex w-full max-w-3xl flex-col gap-6">
<div class="flex flex-col gap-6"> @if($devices->isEmpty())
<div <div class="flex flex-col gap-6">
class="rounded-xl border bg-white dark:bg-stone-950 dark:border-stone-800 text-stone-800 shadow-xs"> <div
<div class="px-10 py-8"> class="rounded-xl border bg-white dark:bg-stone-950 dark:border-stone-800 text-stone-800 shadow-xs">
<h1 class="text-xl font-medium dark:text-zinc-200">Add your first device</h1> <div class="px-10 py-8">
<flux:button href="{{ route('devices') }}" class="mt-4" icon="plus-circle" variant="primary" <h1 class="text-xl font-medium dark:text-zinc-200">Add your first device</h1>
class="w-full mt-4">Add Device <flux:button href="{{ route('devices') }}" class="mt-4" icon="plus-circle" variant="primary"
</flux:button> class="w-full mt-4">Add Device
</flux:button>
</div>
</div> </div>
</div> </div>
</div> @endif
@endif
@foreach($devices as $device) @foreach($devices as $device)
<div class="flex flex-col gap-6"> <div class="flex flex-col gap-6">
<div <div
class="rounded-xl border bg-white dark:bg-stone-950 dark:border-stone-800 text-stone-800 shadow-xs"> class="rounded-xl border bg-white dark:bg-stone-950 dark:border-stone-800 text-stone-800 shadow-xs">
<div class="px-10 py-8"> <div class="px-10 py-8">
@php @php
$current_image_uuid =$device->current_screen_image; $current_image_uuid =$device->current_screen_image;
file_exists('storage/images/generated/' . $current_image_uuid . '.png') ? $file_extension = 'png' : $file_extension = 'bmp'; 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; $current_image_path = 'storage/images/generated/' . $current_image_uuid . '.' . $file_extension;
@endphp @endphp
<h1 class="text-xl font-medium dark:text-zinc-200">{{ $device->name }}</h1> <h1 class="text-xl font-medium dark:text-zinc-200">{{ $device->name }}</h1>
<p class="text-sm dark:text-zinc-400">{{$device->mac_address}}</p> <p class="text-sm dark:text-zinc-400">{{$device->mac_address}}</p>
@if($current_image_uuid) @if($current_image_uuid)
<flux:separator class="mt-2 mb-4"/> <flux:separator class="mt-2 mb-4"/>
<img src="{{ asset($current_image_path) }}" alt="Current Image"/> <img src="{{ asset($current_image_path) }}" alt="Current Image"/>
@endif @endif
</div>
</div> </div>
</div> </div>
</div> @endforeach
@endforeach </div>
</div> </div>
{{-- @php--}} {{-- @php--}}

View file

@ -2,8 +2,4 @@
use App\Jobs\FetchProxyCloudResponses; use App\Jobs\FetchProxyCloudResponses;
// Artisan::command('inspire', function () { Schedule::job(FetchProxyCloudResponses::class, [])->cron(sprintf('*/%s * * * *', intval(config('services.trmnl.proxy_refresh_minutes', 15))));
// $this->comment(Inspiring::quote());
// })->purpose('Display an inspiring quote')->hourly();
Schedule::job(new FetchProxyCloudResponses)->everyFifteenMinutes();

Binary file not shown.

After

Width:  |  Height:  |  Size: 200 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 153 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 210 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 168 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 204 KiB

View file

@ -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)

View file

@ -0,0 +1,158 @@
<?php
use App\Models\Device;
use App\Models\User;
use Illuminate\Support\Facades\Storage;
use Laravel\Sanctum\Sanctum;
uses(\Illuminate\Foundation\Testing\RefreshDatabase::class);
beforeEach(function () {
Storage::fake('public');
});
test('device can fetch display data with valid credentials', function () {
$device = Device::factory()->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' => '<div>Test markup</div>'
// ]);
//
// $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' => '<div>Test markup</div>',
]);
$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']);
});

View file

@ -0,0 +1,15 @@
<?php
use App\Jobs\FetchProxyCloudResponses;
use Illuminate\Support\Facades\Bus;
test('it dispatches fetch proxy cloud responses job', function () {
// Prevent the job from actually running
Bus::fake();
// Run the command
$this->artisan('trmnl:cloud:proxy')->assertSuccessful();
// Assert that the job was dispatched
Bus::assertDispatched(FetchProxyCloudResponses::class);
});

View file

@ -0,0 +1,13 @@
<?php
use App\Jobs\GenerateScreenJob;
use Illuminate\Support\Facades\Bus;
test('it generates screen with default parameters', function () {
Bus::fake();
$this->artisan('trmnl:screen:generate')
->assertSuccessful();
Bus::assertDispatched(GenerateScreenJob::class);
});

View file

@ -0,0 +1,76 @@
<?php
use App\Models\Device;
uses(\Illuminate\Foundation\Testing\RefreshDatabase::class);
test('device can be created with basic attributes', function () {
$device = Device::factory()->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');
});

View file

@ -0,0 +1,106 @@
<?php
use App\Models\Device;
use App\Models\User;
use Livewire\Volt\Volt;
uses(\Illuminate\Foundation\Testing\RefreshDatabase::class);
test('device management page can be rendered', function () {
$user = User::factory()->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();
});

View file

@ -0,0 +1,140 @@
<?php
use App\Jobs\FetchProxyCloudResponses;
use App\Models\Device;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Storage;
uses(\Illuminate\Foundation\Testing\RefreshDatabase::class);
beforeEach(function () {
Storage::fake('public');
Storage::disk('public')->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);
});
});

View file

@ -0,0 +1,59 @@
<?php
use App\Jobs\GenerateScreenJob;
use App\Models\Device;
use Illuminate\Support\Facades\Storage;
uses(\Illuminate\Foundation\Testing\RefreshDatabase::class);
beforeEach(function () {
Storage::fake('public');
Storage::disk('public')->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, '<div>Test</div>');
$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, '<div>Test</div>');
$job->handle();
Storage::disk('public')->assertExists('/images/generated/.gitignore');
})->skipOnGitHubActions();

View file

@ -15,6 +15,7 @@ pest()->extend(Tests\TestCase::class)
->use(Illuminate\Foundation\Testing\RefreshDatabase::class) ->use(Illuminate\Foundation\Testing\RefreshDatabase::class)
->in('Feature'); ->in('Feature');
registerSpatiePestHelpers();
/* /*
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------
| Expectations | Expectations
@ -26,9 +27,9 @@ pest()->extend(Tests\TestCase::class)
| |
*/ */
expect()->extend('toBeOne', function () { // expect()->extend('toBeOne', function () {
return $this->toBe(1); // return $this->toBe(1);
}); // });
/* /*
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------
@ -41,7 +42,7 @@ expect()->extend('toBeOne', function () {
| |
*/ */
function something() // function something()
{ // {
// .. // // ..
} // }