diff --git a/.devcontainer/cli/Dockerfile b/.devcontainer/cli/Dockerfile
index ab13330..0317097 100644
--- a/.devcontainer/cli/Dockerfile
+++ b/.devcontainer/cli/Dockerfile
@@ -9,8 +9,7 @@ RUN apk add --no-cache composer
# Add Chromium and Image Magick for puppeteer.
RUN apk add --no-cache \
imagemagick-dev \
- chromium \
- libzip-dev
+ chromium
ENV PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium
ENV PUPPETEER_DOCKER=1
@@ -20,7 +19,7 @@ RUN chmod 777 /usr/src/php/ext/imagick
RUN curl -fsSL https://github.com/Imagick/imagick/archive/refs/tags/3.8.0.tar.gz | tar xvz -C "/usr/src/php/ext/imagick" --strip 1
# Install PHP extensions
-RUN docker-php-ext-install imagick zip
+RUN docker-php-ext-install imagick
# Composer uses its php binary, but we want it to use the container's one
RUN rm -f /usr/bin/php84
diff --git a/.devcontainer/fpm/Dockerfile b/.devcontainer/fpm/Dockerfile
index 3e658b6..8c585c8 100644
--- a/.devcontainer/fpm/Dockerfile
+++ b/.devcontainer/fpm/Dockerfile
@@ -14,8 +14,7 @@ RUN apk add --no-cache \
nodejs \
npm \
imagemagick-dev \
- chromium \
- libzip-dev
+ chromium
ENV PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium
ENV PUPPETEER_DOCKER=1
@@ -25,7 +24,7 @@ RUN chmod 777 /usr/src/php/ext/imagick
RUN curl -fsSL https://github.com/Imagick/imagick/archive/refs/tags/3.8.0.tar.gz | tar xvz -C "/usr/src/php/ext/imagick" --strip 1
# Install PHP extensions
-RUN docker-php-ext-install imagick zip
+RUN docker-php-ext-install imagick
RUN rm -f /usr/bin/php84
RUN ln -s /usr/local/bin/php /usr/bin/php84
diff --git a/.gitignore b/.gitignore
index 1f4f617..02f3d78 100644
--- a/.gitignore
+++ b/.gitignore
@@ -29,8 +29,3 @@ yarn-error.log
/.junie/guidelines.md
/CLAUDE.md
/.mcp.json
-/.ai
-.DS_Store
-/boost.json
-/.gemini
-/GEMINI.md
diff --git a/README.md b/README.md
index acb0b5c..20bae5d 100644
--- a/README.md
+++ b/README.md
@@ -3,7 +3,7 @@
[](https://github.com/usetrmnl/byos_laravel/actions/workflows/test.yml)
TRMNL BYOS Laravel is a self-hostable implementation of a TRMNL server, built with Laravel.
-It allows you to manage TRMNL devices, generate screens using **native plugins** (Screens API, Markup), **recipes** (120+ from the [OSS community catalog](https://bnussbau.github.io/trmnl-recipe-catalog/), 600+ from the [TRMNL catalog](https://usetrmnl.com/recipes), or your own), or the **API**, and can also act as a **proxy** for the native cloud service (Core). With over 40k downloads and 160+ stars, it’s the most popular community-driven BYOS.
+It allows you to manage TRMNL devices, generate screens using **native plugins** (Screens API, Markup), **recipes** (100+ from the [OSS community catalog](https://bnussbau.github.io/trmnl-recipe-catalog/), ~500 from the [TRMNL catalog](https://usetrmnl.com/recipes), or your own), or the **API**, and can also act as a **proxy** for the native cloud service (Core). With over 30k downloads and 130+ stars, it’s the most popular community-driven BYOS.


diff --git a/app/Jobs/FetchDeviceModelsJob.php b/app/Jobs/FetchDeviceModelsJob.php
index 475c5c7..cb24d98 100644
--- a/app/Jobs/FetchDeviceModelsJob.php
+++ b/app/Jobs/FetchDeviceModelsJob.php
@@ -199,7 +199,6 @@ final class FetchDeviceModelsJob implements ShouldQueue
'offset_x' => $modelData['offset_x'] ?? 0,
'offset_y' => $modelData['offset_y'] ?? 0,
'published_at' => $modelData['published_at'] ?? null,
- 'kind' => $modelData['kind'] ?? null,
'source' => 'api',
];
diff --git a/app/Models/Device.php b/app/Models/Device.php
index 3583f48..2eeb25b 100644
--- a/app/Models/Device.php
+++ b/app/Models/Device.php
@@ -20,14 +20,6 @@ class Device extends Model
protected $guarded = ['id'];
- /**
- * Set the MAC address attribute, normalizing to uppercase.
- */
- public function setMacAddressAttribute(?string $value): void
- {
- $this->attributes['mac_address'] = $value ? mb_strtoupper($value) : null;
- }
-
protected $casts = [
'battery_notification_sent' => 'boolean',
'proxy_cloud' => 'boolean',
diff --git a/app/Models/Playlist.php b/app/Models/Playlist.php
index b4daf5e..7b55a73 100644
--- a/app/Models/Playlist.php
+++ b/app/Models/Playlist.php
@@ -37,32 +37,21 @@ class Playlist extends Model
return false;
}
- // Get user's timezone or fall back to app timezone
- $timezone = $this->device->user->timezone ?? config('app.timezone');
- $now = now($timezone);
-
- // Check weekday (using timezone-aware time)
- if ($this->weekdays !== null && ! in_array($now->dayOfWeek, $this->weekdays)) {
+ // Check weekday
+ if ($this->weekdays !== null && ! in_array(now()->dayOfWeek, $this->weekdays)) {
return false;
}
if ($this->active_from !== null && $this->active_until !== null) {
- // Create timezone-aware datetime objects for active_from and active_until
- $activeFrom = $now->copy()
- ->setTimeFrom($this->active_from)
- ->timezone($timezone);
-
- $activeUntil = $now->copy()
- ->setTimeFrom($this->active_until)
- ->timezone($timezone);
+ $now = now();
// Handle time ranges that span across midnight
- if ($activeFrom > $activeUntil) {
+ if ($this->active_from > $this->active_until) {
// Time range spans midnight (e.g., 09:01 to 03:58)
- if ($now >= $activeFrom || $now <= $activeUntil) {
+ if ($now >= $this->active_from || $now <= $this->active_until) {
return true;
}
- } elseif ($now >= $activeFrom && $now <= $activeUntil) {
+ } elseif ($now >= $this->active_from && $now <= $this->active_until) {
return true;
}
diff --git a/app/Models/Plugin.php b/app/Models/Plugin.php
index 68f8e7e..2915247 100644
--- a/app/Models/Plugin.php
+++ b/app/Models/Plugin.php
@@ -24,7 +24,6 @@ use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Process;
use Illuminate\Support\Str;
-use InvalidArgumentException;
use Keepsuit\LaravelLiquid\LaravelLiquidExtension;
use Keepsuit\Liquid\Exceptions\LiquidException;
use Keepsuit\Liquid\Extensions\StandardExtension;
@@ -45,8 +44,6 @@ class Plugin extends Model
'no_bleed' => 'boolean',
'dark_mode' => 'boolean',
'preferred_renderer' => 'string',
- 'plugin_type' => 'string',
- 'alias' => 'boolean',
];
protected static function boot()
@@ -58,18 +55,6 @@ class Plugin extends Model
$model->uuid = Str::uuid();
}
});
-
- static::updating(function ($model): void {
- // Reset image cache when markup changes
- if ($model->isDirty('render_markup')) {
- $model->current_image = null;
- }
- });
-
- // Sanitize configuration template on save
- static::saving(function ($model): void {
- $model->sanitizeTemplate();
- });
}
public function user()
@@ -77,25 +62,6 @@ class Plugin extends Model
return $this->belongsTo(User::class);
}
- // sanitize configuration template descriptions and help texts (since they allow HTML rendering)
- protected function sanitizeTemplate(): void
- {
- $template = $this->configuration_template;
-
- if (isset($template['custom_fields']) && is_array($template['custom_fields'])) {
- foreach ($template['custom_fields'] as &$field) {
- if (isset($field['description'])) {
- $field['description'] = \Stevebauman\Purify\Facades\Purify::clean($field['description']);
- }
- if (isset($field['help_text'])) {
- $field['help_text'] = \Stevebauman\Purify\Facades\Purify::clean($field['help_text']);
- }
- }
-
- $this->configuration_template = $template;
- }
- }
-
public function hasMissingRequiredConfigurationFields(): bool
{
if (! isset($this->configuration_template['custom_fields']) || empty($this->configuration_template['custom_fields'])) {
@@ -136,11 +102,6 @@ class Plugin extends Model
public function isDataStale(): bool
{
- // Image webhook plugins don't use data staleness - images are pushed directly
- if ($this->plugin_type === 'image_webhook') {
- return false;
- }
-
if ($this->data_strategy === 'webhook') {
// Treat as stale if any webhook event has occurred in the past hour
return $this->data_payload_updated_at && $this->data_payload_updated_at->gt(now()->subHour());
@@ -154,67 +115,105 @@ class Plugin extends Model
public function updateDataPayload(): void
{
- if ($this->data_strategy !== 'polling' || ! $this->polling_url) {
- return;
- }
- $headers = ['User-Agent' => 'usetrmnl/byos_laravel', 'Accept' => 'application/json'];
+ if ($this->data_strategy === 'polling' && $this->polling_url) {
- // resolve headers
- if ($this->polling_header) {
- $resolvedHeader = $this->resolveLiquidVariables($this->polling_header);
- $headerLines = explode("\n", mb_trim($resolvedHeader));
- foreach ($headerLines as $line) {
- $parts = explode(':', $line, 2);
- if (count($parts) === 2) {
- $headers[mb_trim($parts[0])] = mb_trim($parts[1]);
+ $headers = ['User-Agent' => 'usetrmnl/byos_laravel', 'Accept' => 'application/json'];
+
+ if ($this->polling_header) {
+ // Resolve Liquid variables in the polling header
+ $resolvedHeader = $this->resolveLiquidVariables($this->polling_header);
+ $headerLines = explode("\n", mb_trim($resolvedHeader));
+ foreach ($headerLines as $line) {
+ $parts = explode(':', $line, 2);
+ if (count($parts) === 2) {
+ $headers[mb_trim($parts[0])] = mb_trim($parts[1]);
+ }
}
}
- }
- // resolve and clean URLs
- $resolvedPollingUrls = $this->resolveLiquidVariables($this->polling_url);
- $urls = array_values(array_filter( // array_values ensures 0, 1, 2...
- array_map('trim', explode("\n", $resolvedPollingUrls)),
- fn ($url): bool => filled($url)
- ));
+ // Resolve Liquid variables in the entire polling_url field first, then split by newline
+ $resolvedPollingUrls = $this->resolveLiquidVariables($this->polling_url);
+ $urls = array_filter(
+ array_map('trim', explode("\n", $resolvedPollingUrls)),
+ fn ($url): bool => ! empty($url)
+ );
- $combinedResponse = [];
+ // If only one URL, use the original logic without nesting
+ if (count($urls) === 1) {
+ $url = reset($urls);
+ $httpRequest = Http::withHeaders($headers);
- // Loop through all URLs (Handles 1 or many)
- foreach ($urls as $index => $url) {
- $httpRequest = Http::withHeaders($headers);
-
- if ($this->polling_verb === 'post' && $this->polling_body) {
- $resolvedBody = $this->resolveLiquidVariables($this->polling_body);
- $httpRequest = $httpRequest->withBody($resolvedBody);
- }
-
- try {
- $httpResponse = ($this->polling_verb === 'post')
- ? $httpRequest->post($url)
- : $httpRequest->get($url);
-
- $response = $this->parseResponse($httpResponse);
-
- // Nest if it's a sequential array
- if (array_keys($response) === range(0, count($response) - 1)) {
- $combinedResponse["IDX_{$index}"] = ['data' => $response];
- } else {
- $combinedResponse["IDX_{$index}"] = $response;
+ if ($this->polling_verb === 'post' && $this->polling_body) {
+ // Resolve Liquid variables in the polling body
+ $resolvedBody = $this->resolveLiquidVariables($this->polling_body);
+ $httpRequest = $httpRequest->withBody($resolvedBody);
}
- } catch (Exception $e) {
- Log::warning("Failed to fetch data from URL {$url}: ".$e->getMessage());
- $combinedResponse["IDX_{$index}"] = ['error' => 'Failed to fetch data'];
+
+ // URL is already resolved, use it directly
+ $resolvedUrl = $url;
+
+ try {
+ // Make the request based on the verb
+ $httpResponse = $this->polling_verb === 'post' ? $httpRequest->post($resolvedUrl) : $httpRequest->get($resolvedUrl);
+
+ $response = $this->parseResponse($httpResponse);
+
+ $this->update([
+ 'data_payload' => $response,
+ 'data_payload_updated_at' => now(),
+ ]);
+ } catch (Exception $e) {
+ Log::warning("Failed to fetch data from URL {$resolvedUrl}: ".$e->getMessage());
+ $this->update([
+ 'data_payload' => ['error' => 'Failed to fetch data'],
+ 'data_payload_updated_at' => now(),
+ ]);
+ }
+
+ return;
}
+
+ // Multiple URLs - use nested response logic
+ $combinedResponse = [];
+
+ foreach ($urls as $index => $url) {
+ $httpRequest = Http::withHeaders($headers);
+
+ if ($this->polling_verb === 'post' && $this->polling_body) {
+ // Resolve Liquid variables in the polling body
+ $resolvedBody = $this->resolveLiquidVariables($this->polling_body);
+ $httpRequest = $httpRequest->withBody($resolvedBody);
+ }
+
+ // URL is already resolved, use it directly
+ $resolvedUrl = $url;
+
+ try {
+ // Make the request based on the verb
+ $httpResponse = $this->polling_verb === 'post' ? $httpRequest->post($resolvedUrl) : $httpRequest->get($resolvedUrl);
+
+ $response = $this->parseResponse($httpResponse);
+
+ // Check if response is an array at root level
+ if (array_keys($response) === range(0, count($response) - 1)) {
+ // Response is a sequential array, nest under .data
+ $combinedResponse["IDX_{$index}"] = ['data' => $response];
+ } else {
+ // Response is an object or associative array, keep as is
+ $combinedResponse["IDX_{$index}"] = $response;
+ }
+ } catch (Exception $e) {
+ // Log error and continue with other URLs
+ Log::warning("Failed to fetch data from URL {$resolvedUrl}: ".$e->getMessage());
+ $combinedResponse["IDX_{$index}"] = ['error' => 'Failed to fetch data'];
+ }
+ }
+
+ $this->update([
+ 'data_payload' => $combinedResponse,
+ 'data_payload_updated_at' => now(),
+ ]);
}
-
- // unwrap IDX_0 if only one URL
- $finalPayload = (count($urls) === 1) ? reset($combinedResponse) : $combinedResponse;
-
- $this->update([
- 'data_payload' => $finalPayload,
- 'data_payload_updated_at' => now(),
- ]);
}
private function parseResponse(Response $httpResponse): array
@@ -417,10 +416,6 @@ class Plugin extends Model
*/
public function render(string $size = 'full', bool $standalone = true, ?Device $device = null): string
{
- if ($this->plugin_type !== 'recipe') {
- throw new InvalidArgumentException('Render method is only applicable for recipe plugins.');
- }
-
if ($this->render_markup) {
$renderedContent = '';
@@ -528,30 +523,17 @@ class Plugin extends Model
if ($this->render_markup_view) {
if ($standalone) {
- $renderedView = view($this->render_markup_view, [
- 'size' => $size,
- 'data' => $this->data_payload,
- 'config' => $this->configuration ?? [],
- ])->render();
-
- if ($size === 'full') {
- return view('trmnl-layouts.single', [
- 'colorDepth' => $device?->colorDepth(),
- 'deviceVariant' => $device?->deviceVariant() ?? 'og',
- 'noBleed' => $this->no_bleed,
- 'darkMode' => $this->dark_mode,
- 'scaleLevel' => $device?->scaleLevel(),
- 'slot' => $renderedView,
- ])->render();
- }
-
- return view('trmnl-layouts.mashup', [
- 'mashupLayout' => $this->getPreviewMashupLayoutForSize($size),
+ return view('trmnl-layouts.single', [
'colorDepth' => $device?->colorDepth(),
'deviceVariant' => $device?->deviceVariant() ?? 'og',
+ 'noBleed' => $this->no_bleed,
'darkMode' => $this->dark_mode,
'scaleLevel' => $device?->scaleLevel(),
- 'slot' => $renderedView,
+ 'slot' => view($this->render_markup_view, [
+ 'size' => $size,
+ 'data' => $this->data_payload,
+ 'config' => $this->configuration ?? [],
+ ])->render(),
])->render();
}
@@ -582,61 +564,4 @@ class Plugin extends Model
default => '1Tx1B',
};
}
-
- /**
- * Duplicate the plugin, copying all attributes and handling render_markup_view
- *
- * @param int|null $userId Optional user ID for the duplicate. If not provided, uses the original plugin's user_id.
- * @return Plugin The newly created duplicate plugin
- */
- public function duplicate(?int $userId = null): self
- {
- // Get all attributes except id and uuid
- // Use toArray() to get cast values (respects JSON casts)
- $attributes = $this->toArray();
- unset($attributes['id'], $attributes['uuid']);
-
- // Handle render_markup_view - copy file content to render_markup
- if ($this->render_markup_view) {
- try {
- $basePath = resource_path('views/'.str_replace('.', '/', $this->render_markup_view));
- $paths = [
- $basePath.'.blade.php',
- $basePath.'.liquid',
- ];
-
- $fileContent = null;
- $markupLanguage = null;
- foreach ($paths as $path) {
- if (file_exists($path)) {
- $fileContent = file_get_contents($path);
- // Determine markup language based on file extension
- $markupLanguage = str_ends_with($path, '.liquid') ? 'liquid' : 'blade';
- break;
- }
- }
-
- if ($fileContent !== null) {
- $attributes['render_markup'] = $fileContent;
- $attributes['markup_language'] = $markupLanguage;
- $attributes['render_markup_view'] = null;
- } else {
- // File doesn't exist, remove the view reference
- $attributes['render_markup_view'] = null;
- }
- } catch (Exception $e) {
- // If file reading fails, remove the view reference
- $attributes['render_markup_view'] = null;
- }
- }
-
- // Append " (Copy)" to the name
- $attributes['name'] = $this->name.' (Copy)';
-
- // Set user_id - use provided userId or fall back to original plugin's user_id
- $attributes['user_id'] = $userId ?? $this->user_id;
-
- // Create and return the new plugin
- return self::create($attributes);
- }
}
diff --git a/app/Services/ImageGenerationService.php b/app/Services/ImageGenerationService.php
index 405ea3f..cdfc9d2 100644
--- a/app/Services/ImageGenerationService.php
+++ b/app/Services/ImageGenerationService.php
@@ -26,44 +26,11 @@ class ImageGenerationService
public static function generateImage(string $markup, $deviceId): string
{
$device = Device::with(['deviceModel', 'palette', 'deviceModel.palette', 'user'])->find($deviceId);
- $uuid = self::generateImageFromModel(
- markup: $markup,
- deviceModel: $device->deviceModel,
- user: $device->user,
- palette: $device->palette ?? $device->deviceModel?->palette,
- device: $device
- );
-
- $device->update(['current_screen_image' => $uuid]);
- Log::info("Device $device->id: updated with new image: $uuid");
-
- return $uuid;
- }
-
- /**
- * Generate an image from markup using a DeviceModel
- *
- * @param string $markup The HTML markup to render
- * @param DeviceModel|null $deviceModel The device model to use for image generation
- * @param \App\Models\User|null $user Optional user for timezone settings
- * @param \App\Models\DevicePalette|null $palette Optional palette, falls back to device model's palette
- * @param Device|null $device Optional device for legacy devices without DeviceModel
- * @return string The UUID of the generated image
- */
- public static function generateImageFromModel(
- string $markup,
- ?DeviceModel $deviceModel = null,
- ?\App\Models\User $user = null,
- ?\App\Models\DevicePalette $palette = null,
- ?Device $device = null
- ): string {
$uuid = Uuid::uuid4()->toString();
try {
- // Get image generation settings from DeviceModel or Device (for legacy devices)
- $imageSettings = $deviceModel
- ? self::getImageSettingsFromModel($deviceModel)
- : ($device ? self::getImageSettings($device) : self::getImageSettingsFromModel(null));
+ // Get image generation settings from DeviceModel if available, otherwise use device settings
+ $imageSettings = self::getImageSettings($device);
$fileExtension = $imageSettings['mime_type'] === 'image/bmp' ? 'bmp' : 'png';
$outputPath = Storage::disk('public')->path('/images/generated/'.$uuid.'.'.$fileExtension);
@@ -78,7 +45,7 @@ class ImageGenerationService
$browserStage->html($markup);
// Set timezone from user or fall back to app timezone
- $timezone = $user?->timezone ?? config('app.timezone');
+ $timezone = $device->user->timezone ?? config('app.timezone');
$browserStage->timezone($timezone);
if (config('app.puppeteer_window_size_strategy') === 'v2') {
@@ -98,12 +65,12 @@ class ImageGenerationService
$browserStage->setBrowsershotOption('args', ['--no-sandbox', '--disable-setuid-sandbox', '--disable-gpu']);
}
- // Get palette from parameter or fallback to device model's default palette
+ // Get palette from device or fallback to device model's default palette
+ $palette = $device->palette ?? $device->deviceModel?->palette;
$colorPalette = null;
+
if ($palette && $palette->colors) {
$colorPalette = $palette->colors;
- } elseif ($deviceModel?->palette && $deviceModel->palette->colors) {
- $colorPalette = $deviceModel->palette->colors;
}
$imageStage = new ImageStage();
@@ -140,7 +107,8 @@ class ImageGenerationService
throw new RuntimeException('Image file is empty: '.$outputPath);
}
- Log::info("Generated image: $uuid");
+ $device->update(['current_screen_image' => $uuid]);
+ Log::info("Device $device->id: updated with new image: $uuid");
return $uuid;
@@ -157,7 +125,22 @@ class ImageGenerationService
{
// If device has a DeviceModel, use its settings
if ($device->deviceModel) {
- return self::getImageSettingsFromModel($device->deviceModel);
+ /** @var DeviceModel $model */
+ $model = $device->deviceModel;
+
+ return [
+ 'width' => $model->width,
+ 'height' => $model->height,
+ 'colors' => $model->colors,
+ 'bit_depth' => $model->bit_depth,
+ 'scale_factor' => $model->scale_factor,
+ 'rotation' => $model->rotation,
+ 'mime_type' => $model->mime_type,
+ 'offset_x' => $model->offset_x,
+ 'offset_y' => $model->offset_y,
+ 'image_format' => self::determineImageFormatFromModel($model),
+ 'use_model_settings' => true,
+ ];
}
// Fallback to device settings
@@ -181,43 +164,6 @@ class ImageGenerationService
];
}
- /**
- * Get image generation settings from a DeviceModel
- */
- private static function getImageSettingsFromModel(?DeviceModel $deviceModel): array
- {
- if ($deviceModel) {
- return [
- 'width' => $deviceModel->width,
- 'height' => $deviceModel->height,
- 'colors' => $deviceModel->colors,
- 'bit_depth' => $deviceModel->bit_depth,
- 'scale_factor' => $deviceModel->scale_factor,
- 'rotation' => $deviceModel->rotation,
- 'mime_type' => $deviceModel->mime_type,
- 'offset_x' => $deviceModel->offset_x,
- 'offset_y' => $deviceModel->offset_y,
- 'image_format' => self::determineImageFormatFromModel($deviceModel),
- 'use_model_settings' => true,
- ];
- }
-
- // Default settings if no device model provided
- return [
- 'width' => 800,
- 'height' => 480,
- 'colors' => 2,
- 'bit_depth' => 1,
- 'scale_factor' => 1.0,
- 'rotation' => 0,
- 'mime_type' => 'image/png',
- 'offset_x' => 0,
- 'offset_y' => 0,
- 'image_format' => ImageFormat::AUTO->value,
- 'use_model_settings' => false,
- ];
- }
-
/**
* Determine the appropriate ImageFormat based on DeviceModel settings
*/
@@ -334,10 +280,6 @@ class ImageGenerationService
public static function resetIfNotCacheable(?Plugin $plugin): void
{
if ($plugin?->id) {
- // Image webhook plugins have finalized images that shouldn't be reset
- if ($plugin->plugin_type === 'image_webhook') {
- return;
- }
// Check if any devices have custom dimensions or use non-standard DeviceModels
$hasCustomDimensions = Device::query()
->where(function ($query): void {
@@ -369,7 +311,7 @@ class ImageGenerationService
public static function getDeviceSpecificDefaultImage(Device $device, string $imageType): ?string
{
// Validate image type
- if (! in_array($imageType, ['setup-logo', 'sleep', 'error'])) {
+ if (! in_array($imageType, ['setup-logo', 'sleep'])) {
return null;
}
@@ -403,10 +345,10 @@ class ImageGenerationService
/**
* Generate a default screen image from Blade template
*/
- public static function generateDefaultScreenImage(Device $device, string $imageType, ?string $pluginName = null): string
+ public static function generateDefaultScreenImage(Device $device, string $imageType): string
{
// Validate image type
- if (! in_array($imageType, ['setup-logo', 'sleep', 'error'])) {
+ if (! in_array($imageType, ['setup-logo', 'sleep'])) {
throw new InvalidArgumentException("Invalid image type: {$imageType}");
}
@@ -423,7 +365,7 @@ class ImageGenerationService
$outputPath = Storage::disk('public')->path('/images/generated/'.$uuid.'.'.$fileExtension);
// Generate HTML from Blade template
- $html = self::generateDefaultScreenHtml($device, $imageType, $pluginName);
+ $html = self::generateDefaultScreenHtml($device, $imageType);
// Create custom Browsershot instance if using AWS Lambda
$browsershotInstance = null;
@@ -503,13 +445,12 @@ class ImageGenerationService
/**
* Generate HTML from Blade template for default screens
*/
- private static function generateDefaultScreenHtml(Device $device, string $imageType, ?string $pluginName = null): string
+ private static function generateDefaultScreenHtml(Device $device, string $imageType): string
{
// Map image type to template name
$templateName = match ($imageType) {
'setup-logo' => 'default-screens.setup',
'sleep' => 'default-screens.sleep',
- 'error' => 'default-screens.error',
default => throw new InvalidArgumentException("Invalid image type: {$imageType}")
};
@@ -520,22 +461,14 @@ class ImageGenerationService
$scaleLevel = $device->scaleLevel();
$darkMode = $imageType === 'sleep'; // Sleep mode uses dark mode, setup uses light mode
- // Build view data
- $viewData = [
+ // Render the Blade template
+ return view($templateName, [
'noBleed' => false,
'darkMode' => $darkMode,
'deviceVariant' => $deviceVariant,
'deviceOrientation' => $deviceOrientation,
'colorDepth' => $colorDepth,
'scaleLevel' => $scaleLevel,
- ];
-
- // Add plugin name for error screens
- if ($imageType === 'error' && $pluginName !== null) {
- $viewData['pluginName'] = $pluginName;
- }
-
- // Render the Blade template
- return view($templateName, $viewData)->render();
+ ])->render();
}
}
diff --git a/app/Services/Plugin/Parsers/IcalResponseParser.php b/app/Services/Plugin/Parsers/IcalResponseParser.php
index c8f2b58..f87e71c 100644
--- a/app/Services/Plugin/Parsers/IcalResponseParser.php
+++ b/app/Services/Plugin/Parsers/IcalResponseParser.php
@@ -34,7 +34,7 @@ class IcalResponseParser implements ResponseParser
$filteredEvents = array_values(array_filter($events, function (array $event) use ($windowStart, $windowEnd): bool {
$startDate = $this->asCarbon($event['DTSTART'] ?? null);
- if (! $startDate instanceof Carbon) {
+ if (!$startDate instanceof \Carbon\Carbon) {
return false;
}
diff --git a/app/Services/PluginImportService.php b/app/Services/PluginImportService.php
index 49dce99..9207e3e 100644
--- a/app/Services/PluginImportService.php
+++ b/app/Services/PluginImportService.php
@@ -17,34 +17,6 @@ use ZipArchive;
class PluginImportService
{
- /**
- * Validate YAML settings
- *
- * @param array $settings The parsed YAML settings
- *
- * @throws Exception
- */
- private function validateYAML(array $settings): void
- {
- if (! isset($settings['custom_fields']) || ! is_array($settings['custom_fields'])) {
- return;
- }
-
- foreach ($settings['custom_fields'] as $field) {
- if (isset($field['field_type']) && $field['field_type'] === 'multi_string') {
-
- if (isset($field['default']) && str_contains($field['default'], ',')) {
- throw new Exception("Validation Error: The default value for multistring fields like `{$field['keyname']}` cannot contain commas.");
- }
-
- if (isset($field['placeholder']) && str_contains($field['placeholder'], ',')) {
- throw new Exception("Validation Error: The placeholder value for multistring fields like `{$field['keyname']}` cannot contain commas.");
- }
-
- }
- }
- }
-
/**
* Import a plugin from a ZIP file
*
@@ -75,55 +47,32 @@ class PluginImportService
$zip->extractTo($tempDir);
$zip->close();
- // Find the required files (settings.yml and full.liquid/full.blade.php/shared.liquid/shared.blade.php)
+ // Find the required files (settings.yml and full.liquid/full.blade.php)
$filePaths = $this->findRequiredFiles($tempDir, $zipEntryPath);
// Validate that we found the required files
- if (! $filePaths['settingsYamlPath']) {
- throw new Exception('Invalid ZIP structure. Required file settings.yml is missing.');
- }
-
- // Validate that we have at least one template file
- if (! $filePaths['fullLiquidPath'] && ! $filePaths['sharedLiquidPath'] && ! $filePaths['sharedBladePath']) {
- throw new Exception('Invalid ZIP structure. At least one of the following files is required: full.liquid, full.blade.php, shared.liquid, or shared.blade.php.');
+ if (! $filePaths['settingsYamlPath'] || ! $filePaths['fullLiquidPath']) {
+ throw new Exception('Invalid ZIP structure. Required files settings.yml and full.liquid are missing.'); // full.blade.php
}
// Parse settings.yml
$settingsYaml = File::get($filePaths['settingsYamlPath']);
$settings = Yaml::parse($settingsYaml);
- $this->validateYAML($settings);
- // Determine which template file to use and read its content
- $templatePath = null;
+ // Read full.liquid content
+ $fullLiquid = File::get($filePaths['fullLiquidPath']);
+
+ // Prepend shared.liquid content if available
+ if ($filePaths['sharedLiquidPath'] && File::exists($filePaths['sharedLiquidPath'])) {
+ $sharedLiquid = File::get($filePaths['sharedLiquidPath']);
+ $fullLiquid = $sharedLiquid."\n".$fullLiquid;
+ }
+
+ // Check if the file ends with .liquid to set markup language
$markupLanguage = 'blade';
-
- if ($filePaths['fullLiquidPath']) {
- $templatePath = $filePaths['fullLiquidPath'];
- $fullLiquid = File::get($templatePath);
-
- // Prepend shared.liquid or shared.blade.php content if available
- if ($filePaths['sharedLiquidPath'] && File::exists($filePaths['sharedLiquidPath'])) {
- $sharedLiquid = File::get($filePaths['sharedLiquidPath']);
- $fullLiquid = $sharedLiquid."\n".$fullLiquid;
- } elseif ($filePaths['sharedBladePath'] && File::exists($filePaths['sharedBladePath'])) {
- $sharedBlade = File::get($filePaths['sharedBladePath']);
- $fullLiquid = $sharedBlade."\n".$fullLiquid;
- }
-
- // Check if the file ends with .liquid to set markup language
- if (pathinfo((string) $templatePath, PATHINFO_EXTENSION) === 'liquid') {
- $markupLanguage = 'liquid';
- $fullLiquid = '
'."\n".$fullLiquid."\n".'
';
- }
- } elseif ($filePaths['sharedLiquidPath']) {
- $templatePath = $filePaths['sharedLiquidPath'];
- $fullLiquid = File::get($templatePath);
+ if (pathinfo((string) $filePaths['fullLiquidPath'], PATHINFO_EXTENSION) === 'liquid') {
$markupLanguage = 'liquid';
$fullLiquid = ''."\n".$fullLiquid."\n".'
';
- } elseif ($filePaths['sharedBladePath']) {
- $templatePath = $filePaths['sharedBladePath'];
- $fullLiquid = File::get($templatePath);
- $markupLanguage = 'blade';
}
// Ensure custom_fields is properly formatted
@@ -195,12 +144,11 @@ class PluginImportService
* @param string|null $zipEntryPath Optional path to specific plugin in monorepo
* @param string|null $preferredRenderer Optional preferred renderer (e.g., 'trmnl-liquid')
* @param string|null $iconUrl Optional icon URL to set on the plugin
- * @param bool $allowDuplicate If true, generate a new UUID for trmnlp_id if a plugin with the same trmnlp_id already exists
* @return Plugin The created plugin instance
*
* @throws Exception If the ZIP file is invalid or required files are missing
*/
- public function importFromUrl(string $zipUrl, User $user, ?string $zipEntryPath = null, $preferredRenderer = null, ?string $iconUrl = null, bool $allowDuplicate = false): Plugin
+ public function importFromUrl(string $zipUrl, User $user, ?string $zipEntryPath = null, $preferredRenderer = null, ?string $iconUrl = null): Plugin
{
// Download the ZIP file
$response = Http::timeout(60)->get($zipUrl);
@@ -228,55 +176,32 @@ class PluginImportService
$zip->extractTo($tempDir);
$zip->close();
- // Find the required files (settings.yml and full.liquid/full.blade.php/shared.liquid/shared.blade.php)
+ // Find the required files (settings.yml and full.liquid/full.blade.php)
$filePaths = $this->findRequiredFiles($tempDir, $zipEntryPath);
// Validate that we found the required files
- if (! $filePaths['settingsYamlPath']) {
- throw new Exception('Invalid ZIP structure. Required file settings.yml is missing.');
- }
-
- // Validate that we have at least one template file
- if (! $filePaths['fullLiquidPath'] && ! $filePaths['sharedLiquidPath'] && ! $filePaths['sharedBladePath']) {
- throw new Exception('Invalid ZIP structure. At least one of the following files is required: full.liquid, full.blade.php, shared.liquid, or shared.blade.php.');
+ if (! $filePaths['settingsYamlPath'] || ! $filePaths['fullLiquidPath']) {
+ throw new Exception('Invalid ZIP structure. Required files settings.yml and full.liquid/full.blade.php are missing.');
}
// Parse settings.yml
$settingsYaml = File::get($filePaths['settingsYamlPath']);
$settings = Yaml::parse($settingsYaml);
- $this->validateYAML($settings);
- // Determine which template file to use and read its content
- $templatePath = null;
+ // Read full.liquid content
+ $fullLiquid = File::get($filePaths['fullLiquidPath']);
+
+ // Prepend shared.liquid content if available
+ if ($filePaths['sharedLiquidPath'] && File::exists($filePaths['sharedLiquidPath'])) {
+ $sharedLiquid = File::get($filePaths['sharedLiquidPath']);
+ $fullLiquid = $sharedLiquid."\n".$fullLiquid;
+ }
+
+ // Check if the file ends with .liquid to set markup language
$markupLanguage = 'blade';
-
- if ($filePaths['fullLiquidPath']) {
- $templatePath = $filePaths['fullLiquidPath'];
- $fullLiquid = File::get($templatePath);
-
- // Prepend shared.liquid or shared.blade.php content if available
- if ($filePaths['sharedLiquidPath'] && File::exists($filePaths['sharedLiquidPath'])) {
- $sharedLiquid = File::get($filePaths['sharedLiquidPath']);
- $fullLiquid = $sharedLiquid."\n".$fullLiquid;
- } elseif ($filePaths['sharedBladePath'] && File::exists($filePaths['sharedBladePath'])) {
- $sharedBlade = File::get($filePaths['sharedBladePath']);
- $fullLiquid = $sharedBlade."\n".$fullLiquid;
- }
-
- // Check if the file ends with .liquid to set markup language
- if (pathinfo((string) $templatePath, PATHINFO_EXTENSION) === 'liquid') {
- $markupLanguage = 'liquid';
- $fullLiquid = ''."\n".$fullLiquid."\n".'
';
- }
- } elseif ($filePaths['sharedLiquidPath']) {
- $templatePath = $filePaths['sharedLiquidPath'];
- $fullLiquid = File::get($templatePath);
+ if (pathinfo((string) $filePaths['fullLiquidPath'], PATHINFO_EXTENSION) === 'liquid') {
$markupLanguage = 'liquid';
$fullLiquid = ''."\n".$fullLiquid."\n".'
';
- } elseif ($filePaths['sharedBladePath']) {
- $templatePath = $filePaths['sharedBladePath'];
- $fullLiquid = File::get($templatePath);
- $markupLanguage = 'blade';
}
// Ensure custom_fields is properly formatted
@@ -292,26 +217,17 @@ class PluginImportService
'custom_fields' => $settings['custom_fields'],
];
- // Determine the trmnlp_id to use
- $trmnlpId = $settings['id'] ?? Uuid::v7();
-
- // If allowDuplicate is true and a plugin with this trmnlp_id already exists, generate a new UUID
- if ($allowDuplicate && isset($settings['id']) && Plugin::where('user_id', $user->id)->where('trmnlp_id', $settings['id'])->exists()) {
- $trmnlpId = Uuid::v7();
- }
-
- $plugin_updated = ! $allowDuplicate && isset($settings['id'])
+ $plugin_updated = isset($settings['id'])
&& Plugin::where('user_id', $user->id)->where('trmnlp_id', $settings['id'])->exists();
-
// Create a new plugin
$plugin = Plugin::updateOrCreate(
[
- 'user_id' => $user->id, 'trmnlp_id' => $trmnlpId,
+ 'user_id' => $user->id, 'trmnlp_id' => $settings['id'] ?? Uuid::v7(),
],
[
'user_id' => $user->id,
'name' => $settings['name'] ?? 'Imported Plugin',
- 'trmnlp_id' => $trmnlpId,
+ 'trmnlp_id' => $settings['id'] ?? Uuid::v7(),
'data_stale_minutes' => $settings['refresh_interval'] ?? 15,
'data_strategy' => $settings['strategy'] ?? 'static',
'polling_url' => $settings['polling_url'] ?? null,
@@ -356,7 +272,6 @@ class PluginImportService
$settingsYamlPath = null;
$fullLiquidPath = null;
$sharedLiquidPath = null;
- $sharedBladePath = null;
// If zipEntryPath is specified, look for files in that specific directory first
if ($zipEntryPath) {
@@ -374,8 +289,6 @@ class PluginImportService
if (File::exists($targetDir.'/shared.liquid')) {
$sharedLiquidPath = $targetDir.'/shared.liquid';
- } elseif (File::exists($targetDir.'/shared.blade.php')) {
- $sharedBladePath = $targetDir.'/shared.blade.php';
}
}
@@ -391,18 +304,15 @@ class PluginImportService
if (File::exists($targetDir.'/src/shared.liquid')) {
$sharedLiquidPath = $targetDir.'/src/shared.liquid';
- } elseif (File::exists($targetDir.'/src/shared.blade.php')) {
- $sharedBladePath = $targetDir.'/src/shared.blade.php';
}
}
// If we found the required files in the target directory, return them
- if ($settingsYamlPath && ($fullLiquidPath || $sharedLiquidPath || $sharedBladePath)) {
+ if ($settingsYamlPath && $fullLiquidPath) {
return [
'settingsYamlPath' => $settingsYamlPath,
'fullLiquidPath' => $fullLiquidPath,
'sharedLiquidPath' => $sharedLiquidPath,
- 'sharedBladePath' => $sharedBladePath,
];
}
}
@@ -419,11 +329,9 @@ class PluginImportService
$fullLiquidPath = $tempDir.'/src/full.blade.php';
}
- // Check for shared.liquid or shared.blade.php in the same directory
+ // Check for shared.liquid in the same directory
if (File::exists($tempDir.'/src/shared.liquid')) {
$sharedLiquidPath = $tempDir.'/src/shared.liquid';
- } elseif (File::exists($tempDir.'/src/shared.blade.php')) {
- $sharedBladePath = $tempDir.'/src/shared.blade.php';
}
} else {
// Search for the files in the extracted directory structure
@@ -440,24 +348,20 @@ class PluginImportService
$fullLiquidPath = $filepath;
} elseif ($filename === 'shared.liquid') {
$sharedLiquidPath = $filepath;
- } elseif ($filename === 'shared.blade.php') {
- $sharedBladePath = $filepath;
}
}
- // Check if shared.liquid or shared.blade.php exists in the same directory as full.liquid
- if ($settingsYamlPath && $fullLiquidPath && ! $sharedLiquidPath && ! $sharedBladePath) {
+ // Check if shared.liquid exists in the same directory as full.liquid
+ if ($settingsYamlPath && $fullLiquidPath && ! $sharedLiquidPath) {
$fullLiquidDir = dirname((string) $fullLiquidPath);
if (File::exists($fullLiquidDir.'/shared.liquid')) {
$sharedLiquidPath = $fullLiquidDir.'/shared.liquid';
- } elseif (File::exists($fullLiquidDir.'/shared.blade.php')) {
- $sharedBladePath = $fullLiquidDir.'/shared.blade.php';
}
}
// If we found the files but they're not in the src folder,
// check if they're in the root of the ZIP or in a subfolder
- if ($settingsYamlPath && ($fullLiquidPath || $sharedLiquidPath || $sharedBladePath)) {
+ if ($settingsYamlPath && $fullLiquidPath) {
// If the files are in the root of the ZIP, create a src folder and move them there
$srcDir = dirname((string) $settingsYamlPath);
@@ -468,25 +372,17 @@ class PluginImportService
// Copy the files to the src directory
File::copy($settingsYamlPath, $newSrcDir.'/settings.yml');
+ File::copy($fullLiquidPath, $newSrcDir.'/full.liquid');
- // Copy full.liquid or full.blade.php if it exists
- if ($fullLiquidPath) {
- $extension = pathinfo((string) $fullLiquidPath, PATHINFO_EXTENSION);
- File::copy($fullLiquidPath, $newSrcDir.'/full.'.$extension);
- $fullLiquidPath = $newSrcDir.'/full.'.$extension;
- }
-
- // Copy shared.liquid or shared.blade.php if it exists
+ // Copy shared.liquid if it exists
if ($sharedLiquidPath) {
File::copy($sharedLiquidPath, $newSrcDir.'/shared.liquid');
$sharedLiquidPath = $newSrcDir.'/shared.liquid';
- } elseif ($sharedBladePath) {
- File::copy($sharedBladePath, $newSrcDir.'/shared.blade.php');
- $sharedBladePath = $newSrcDir.'/shared.blade.php';
}
// Update the paths
$settingsYamlPath = $newSrcDir.'/settings.yml';
+ $fullLiquidPath = $newSrcDir.'/full.liquid';
}
}
}
@@ -495,7 +391,6 @@ class PluginImportService
'settingsYamlPath' => $settingsYamlPath,
'fullLiquidPath' => $fullLiquidPath,
'sharedLiquidPath' => $sharedLiquidPath,
- 'sharedBladePath' => $sharedBladePath,
];
}
diff --git a/boost.json b/boost.json
new file mode 100644
index 0000000..53962fa
--- /dev/null
+++ b/boost.json
@@ -0,0 +1,15 @@
+{
+ "agents": [
+ "claude_code",
+ "copilot",
+ "cursor",
+ "phpstorm"
+ ],
+ "editors": [
+ "claude_code",
+ "cursor",
+ "phpstorm",
+ "vscode"
+ ],
+ "guidelines": []
+}
diff --git a/composer.json b/composer.json
index 0ced4da..2281415 100644
--- a/composer.json
+++ b/composer.json
@@ -6,7 +6,6 @@
"keywords": [
"trmnl",
"trmnl-server",
- "trmnl-byos",
"laravel"
],
"license": "MIT",
@@ -15,7 +14,7 @@
"ext-imagick": "*",
"ext-simplexml": "*",
"ext-zip": "*",
- "bnussbau/laravel-trmnl-blade": "2.1.*",
+ "bnussbau/laravel-trmnl-blade": "2.0.*",
"bnussbau/trmnl-pipeline-php": "^0.6.0",
"keepsuit/laravel-liquid": "^0.5.2",
"laravel/framework": "^12.1",
@@ -26,7 +25,6 @@
"livewire/volt": "^1.7",
"om/icalparser": "^3.2",
"spatie/browsershot": "^5.0",
- "stevebauman/purify": "^6.3",
"symfony/yaml": "^7.3",
"wnx/sidecar-browsershot": "^2.6"
},
diff --git a/composer.lock b/composer.lock
index d23d014..9d34443 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": "25c2a1a4a2f2594adefe25ddb6a072fb",
+ "content-hash": "3e4c22c016c04e49512b5fcd20983baa",
"packages": [
{
"name": "aws/aws-crt-php",
@@ -62,16 +62,16 @@
},
{
"name": "aws/aws-sdk-php",
- "version": "3.369.10",
+ "version": "3.366.1",
"source": {
"type": "git",
"url": "https://github.com/aws/aws-sdk-php.git",
- "reference": "e179090bf2d658be7be37afc146111966ba6f41b"
+ "reference": "981ae91529b990987bdb182c11322dd34848976a"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/e179090bf2d658be7be37afc146111966ba6f41b",
- "reference": "e179090bf2d658be7be37afc146111966ba6f41b",
+ "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/981ae91529b990987bdb182c11322dd34848976a",
+ "reference": "981ae91529b990987bdb182c11322dd34848976a",
"shasum": ""
},
"require": {
@@ -85,7 +85,7 @@
"mtdowling/jmespath.php": "^2.8.0",
"php": ">=8.1",
"psr/http-message": "^1.0 || ^2.0",
- "symfony/filesystem": "^v5.4.45 || ^v6.4.3 || ^v7.1.0 || ^v8.0.0"
+ "symfony/filesystem": "^v6.4.3 || ^v7.1.0 || ^v8.0.0"
},
"require-dev": {
"andrewsville/php-token-reflection": "^1.4",
@@ -153,22 +153,22 @@
"support": {
"forum": "https://github.com/aws/aws-sdk-php/discussions",
"issues": "https://github.com/aws/aws-sdk-php/issues",
- "source": "https://github.com/aws/aws-sdk-php/tree/3.369.10"
+ "source": "https://github.com/aws/aws-sdk-php/tree/3.366.1"
},
- "time": "2026-01-09T19:08:12+00:00"
+ "time": "2025-12-04T16:55:00+00:00"
},
{
"name": "bnussbau/laravel-trmnl-blade",
- "version": "2.1.0",
+ "version": "2.0.1",
"source": {
"type": "git",
"url": "https://github.com/bnussbau/laravel-trmnl-blade.git",
- "reference": "1e1cabfead00118d7a80c86ac6108aece2989bc7"
+ "reference": "59343cfa9c41c7c7f9285b366584cde92bf1294e"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/bnussbau/laravel-trmnl-blade/zipball/1e1cabfead00118d7a80c86ac6108aece2989bc7",
- "reference": "1e1cabfead00118d7a80c86ac6108aece2989bc7",
+ "url": "https://api.github.com/repos/bnussbau/laravel-trmnl-blade/zipball/59343cfa9c41c7c7f9285b366584cde92bf1294e",
+ "reference": "59343cfa9c41c7c7f9285b366584cde92bf1294e",
"shasum": ""
},
"require": {
@@ -223,7 +223,7 @@
],
"support": {
"issues": "https://github.com/bnussbau/laravel-trmnl-blade/issues",
- "source": "https://github.com/bnussbau/laravel-trmnl-blade/tree/2.1.0"
+ "source": "https://github.com/bnussbau/laravel-trmnl-blade/tree/2.0.1"
},
"funding": [
{
@@ -239,7 +239,7 @@
"type": "github"
}
],
- "time": "2026-01-02T20:38:51+00:00"
+ "time": "2025-09-22T12:12:00+00:00"
},
{
"name": "bnussbau/trmnl-pipeline-php",
@@ -814,79 +814,18 @@
],
"time": "2025-03-06T22:45:56+00:00"
},
- {
- "name": "ezyang/htmlpurifier",
- "version": "v4.19.0",
- "source": {
- "type": "git",
- "url": "https://github.com/ezyang/htmlpurifier.git",
- "reference": "b287d2a16aceffbf6e0295559b39662612b77fcf"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/ezyang/htmlpurifier/zipball/b287d2a16aceffbf6e0295559b39662612b77fcf",
- "reference": "b287d2a16aceffbf6e0295559b39662612b77fcf",
- "shasum": ""
- },
- "require": {
- "php": "~5.6.0 || ~7.0.0 || ~7.1.0 || ~7.2.0 || ~7.3.0 || ~7.4.0 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0"
- },
- "require-dev": {
- "cerdic/css-tidy": "^1.7 || ^2.0",
- "simpletest/simpletest": "dev-master"
- },
- "suggest": {
- "cerdic/css-tidy": "If you want to use the filter 'Filter.ExtractStyleBlocks'.",
- "ext-bcmath": "Used for unit conversion and imagecrash protection",
- "ext-iconv": "Converts text to and from non-UTF-8 encodings",
- "ext-tidy": "Used for pretty-printing HTML"
- },
- "type": "library",
- "autoload": {
- "files": [
- "library/HTMLPurifier.composer.php"
- ],
- "psr-0": {
- "HTMLPurifier": "library/"
- },
- "exclude-from-classmap": [
- "/library/HTMLPurifier/Language/"
- ]
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "LGPL-2.1-or-later"
- ],
- "authors": [
- {
- "name": "Edward Z. Yang",
- "email": "admin@htmlpurifier.org",
- "homepage": "http://ezyang.com"
- }
- ],
- "description": "Standards compliant HTML filter written in PHP",
- "homepage": "http://htmlpurifier.org/",
- "keywords": [
- "html"
- ],
- "support": {
- "issues": "https://github.com/ezyang/htmlpurifier/issues",
- "source": "https://github.com/ezyang/htmlpurifier/tree/v4.19.0"
- },
- "time": "2025-10-17T16:34:55+00:00"
- },
{
"name": "firebase/php-jwt",
- "version": "v7.0.2",
+ "version": "v6.11.1",
"source": {
"type": "git",
"url": "https://github.com/firebase/php-jwt.git",
- "reference": "5645b43af647b6947daac1d0f659dd1fbe8d3b65"
+ "reference": "d1e91ecf8c598d073d0995afa8cd5c75c6e19e66"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/firebase/php-jwt/zipball/5645b43af647b6947daac1d0f659dd1fbe8d3b65",
- "reference": "5645b43af647b6947daac1d0f659dd1fbe8d3b65",
+ "url": "https://api.github.com/repos/firebase/php-jwt/zipball/d1e91ecf8c598d073d0995afa8cd5c75c6e19e66",
+ "reference": "d1e91ecf8c598d073d0995afa8cd5c75c6e19e66",
"shasum": ""
},
"require": {
@@ -934,9 +873,9 @@
],
"support": {
"issues": "https://github.com/firebase/php-jwt/issues",
- "source": "https://github.com/firebase/php-jwt/tree/v7.0.2"
+ "source": "https://github.com/firebase/php-jwt/tree/v6.11.1"
},
- "time": "2025-12-16T22:17:28+00:00"
+ "time": "2025-04-09T20:32:01+00:00"
},
{
"name": "fruitcake/php-cors",
@@ -1011,24 +950,24 @@
},
{
"name": "graham-campbell/result-type",
- "version": "v1.1.4",
+ "version": "v1.1.3",
"source": {
"type": "git",
"url": "https://github.com/GrahamCampbell/Result-Type.git",
- "reference": "e01f4a821471308ba86aa202fed6698b6b695e3b"
+ "reference": "3ba905c11371512af9d9bdd27d99b782216b6945"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/GrahamCampbell/Result-Type/zipball/e01f4a821471308ba86aa202fed6698b6b695e3b",
- "reference": "e01f4a821471308ba86aa202fed6698b6b695e3b",
+ "url": "https://api.github.com/repos/GrahamCampbell/Result-Type/zipball/3ba905c11371512af9d9bdd27d99b782216b6945",
+ "reference": "3ba905c11371512af9d9bdd27d99b782216b6945",
"shasum": ""
},
"require": {
"php": "^7.2.5 || ^8.0",
- "phpoption/phpoption": "^1.9.5"
+ "phpoption/phpoption": "^1.9.3"
},
"require-dev": {
- "phpunit/phpunit": "^8.5.41 || ^9.6.22 || ^10.5.45 || ^11.5.7"
+ "phpunit/phpunit": "^8.5.39 || ^9.6.20 || ^10.5.28"
},
"type": "library",
"autoload": {
@@ -1057,7 +996,7 @@
],
"support": {
"issues": "https://github.com/GrahamCampbell/Result-Type/issues",
- "source": "https://github.com/GrahamCampbell/Result-Type/tree/v1.1.4"
+ "source": "https://github.com/GrahamCampbell/Result-Type/tree/v1.1.3"
},
"funding": [
{
@@ -1069,7 +1008,7 @@
"type": "tidelift"
}
],
- "time": "2025-12-27T19:43:20+00:00"
+ "time": "2024-07-20T21:45:45+00:00"
},
{
"name": "guzzlehttp/guzzle",
@@ -1678,16 +1617,16 @@
},
{
"name": "laravel/framework",
- "version": "v12.46.0",
+ "version": "v12.41.1",
"source": {
"type": "git",
"url": "https://github.com/laravel/framework.git",
- "reference": "9dcff48d25a632c1fadb713024c952fec489c4ae"
+ "reference": "3e229b05935fd0300c632fb1f718c73046d664fc"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/laravel/framework/zipball/9dcff48d25a632c1fadb713024c952fec489c4ae",
- "reference": "9dcff48d25a632c1fadb713024c952fec489c4ae",
+ "url": "https://api.github.com/repos/laravel/framework/zipball/3e229b05935fd0300c632fb1f718c73046d664fc",
+ "reference": "3e229b05935fd0300c632fb1f718c73046d664fc",
"shasum": ""
},
"require": {
@@ -1775,7 +1714,6 @@
"illuminate/process": "self.version",
"illuminate/queue": "self.version",
"illuminate/redis": "self.version",
- "illuminate/reflection": "self.version",
"illuminate/routing": "self.version",
"illuminate/session": "self.version",
"illuminate/support": "self.version",
@@ -1800,7 +1738,7 @@
"league/flysystem-sftp-v3": "^3.25.1",
"mockery/mockery": "^1.6.10",
"opis/json-schema": "^2.4.1",
- "orchestra/testbench-core": "^10.8.1",
+ "orchestra/testbench-core": "^10.8.0",
"pda/pheanstalk": "^5.0.6|^7.0.0",
"php-http/discovery": "^1.15",
"phpstan/phpstan": "^2.0",
@@ -1862,7 +1800,6 @@
"src/Illuminate/Filesystem/functions.php",
"src/Illuminate/Foundation/helpers.php",
"src/Illuminate/Log/functions.php",
- "src/Illuminate/Reflection/helpers.php",
"src/Illuminate/Support/functions.php",
"src/Illuminate/Support/helpers.php"
],
@@ -1871,8 +1808,7 @@
"Illuminate\\Support\\": [
"src/Illuminate/Macroable/",
"src/Illuminate/Collections/",
- "src/Illuminate/Conditionable/",
- "src/Illuminate/Reflection/"
+ "src/Illuminate/Conditionable/"
]
}
},
@@ -1896,7 +1832,7 @@
"issues": "https://github.com/laravel/framework/issues",
"source": "https://github.com/laravel/framework"
},
- "time": "2026-01-07T23:26:53+00:00"
+ "time": "2025-12-03T01:02:13+00:00"
},
{
"name": "laravel/prompts",
@@ -1959,16 +1895,16 @@
},
{
"name": "laravel/sanctum",
- "version": "v4.2.2",
+ "version": "v4.2.1",
"source": {
"type": "git",
"url": "https://github.com/laravel/sanctum.git",
- "reference": "fd447754d2d3f56950d53b930128af2e3b617de9"
+ "reference": "f5fb373be39a246c74a060f2cf2ae2c2145b3664"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/laravel/sanctum/zipball/fd447754d2d3f56950d53b930128af2e3b617de9",
- "reference": "fd447754d2d3f56950d53b930128af2e3b617de9",
+ "url": "https://api.github.com/repos/laravel/sanctum/zipball/f5fb373be39a246c74a060f2cf2ae2c2145b3664",
+ "reference": "f5fb373be39a246c74a060f2cf2ae2c2145b3664",
"shasum": ""
},
"require": {
@@ -2018,7 +1954,7 @@
"issues": "https://github.com/laravel/sanctum/issues",
"source": "https://github.com/laravel/sanctum"
},
- "time": "2026-01-06T23:11:51+00:00"
+ "time": "2025-11-21T13:59:03+00:00"
},
{
"name": "laravel/serializable-closure",
@@ -2083,21 +2019,21 @@
},
{
"name": "laravel/socialite",
- "version": "v5.24.1",
+ "version": "v5.23.2",
"source": {
"type": "git",
"url": "https://github.com/laravel/socialite.git",
- "reference": "25e28c14d55404886777af1d77cf030e0f633142"
+ "reference": "41e65d53762d33d617bf0253330d672cb95e624b"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/laravel/socialite/zipball/25e28c14d55404886777af1d77cf030e0f633142",
- "reference": "25e28c14d55404886777af1d77cf030e0f633142",
+ "url": "https://api.github.com/repos/laravel/socialite/zipball/41e65d53762d33d617bf0253330d672cb95e624b",
+ "reference": "41e65d53762d33d617bf0253330d672cb95e624b",
"shasum": ""
},
"require": {
"ext-json": "*",
- "firebase/php-jwt": "^6.4|^7.0",
+ "firebase/php-jwt": "^6.4",
"guzzlehttp/guzzle": "^6.0|^7.0",
"illuminate/contracts": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0",
"illuminate/http": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0",
@@ -2151,20 +2087,20 @@
"issues": "https://github.com/laravel/socialite/issues",
"source": "https://github.com/laravel/socialite"
},
- "time": "2026-01-01T02:57:21+00:00"
+ "time": "2025-11-21T14:00:38+00:00"
},
{
"name": "laravel/tinker",
- "version": "v2.11.0",
+ "version": "v2.10.2",
"source": {
"type": "git",
"url": "https://github.com/laravel/tinker.git",
- "reference": "3d34b97c9a1747a81a3fde90482c092bd8b66468"
+ "reference": "3bcb5f62d6f837e0f093a601e26badafb127bd4c"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/laravel/tinker/zipball/3d34b97c9a1747a81a3fde90482c092bd8b66468",
- "reference": "3d34b97c9a1747a81a3fde90482c092bd8b66468",
+ "url": "https://api.github.com/repos/laravel/tinker/zipball/3bcb5f62d6f837e0f093a601e26badafb127bd4c",
+ "reference": "3bcb5f62d6f837e0f093a601e26badafb127bd4c",
"shasum": ""
},
"require": {
@@ -2173,7 +2109,7 @@
"illuminate/support": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0",
"php": "^7.2.5|^8.0",
"psy/psysh": "^0.11.1|^0.12.0",
- "symfony/var-dumper": "^4.3.4|^5.0|^6.0|^7.0|^8.0"
+ "symfony/var-dumper": "^4.3.4|^5.0|^6.0|^7.0"
},
"require-dev": {
"mockery/mockery": "~1.3.3|^1.4.2",
@@ -2215,9 +2151,9 @@
],
"support": {
"issues": "https://github.com/laravel/tinker/issues",
- "source": "https://github.com/laravel/tinker/tree/v2.11.0"
+ "source": "https://github.com/laravel/tinker/tree/v2.10.2"
},
- "time": "2025-12-19T19:16:45+00:00"
+ "time": "2025-11-20T16:29:12+00:00"
},
{
"name": "league/commonmark",
@@ -2730,20 +2666,20 @@
},
{
"name": "league/uri",
- "version": "7.7.0",
+ "version": "7.6.0",
"source": {
"type": "git",
"url": "https://github.com/thephpleague/uri.git",
- "reference": "8d587cddee53490f9b82bf203d3a9aa7ea4f9807"
+ "reference": "f625804987a0a9112d954f9209d91fec52182344"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/thephpleague/uri/zipball/8d587cddee53490f9b82bf203d3a9aa7ea4f9807",
- "reference": "8d587cddee53490f9b82bf203d3a9aa7ea4f9807",
+ "url": "https://api.github.com/repos/thephpleague/uri/zipball/f625804987a0a9112d954f9209d91fec52182344",
+ "reference": "f625804987a0a9112d954f9209d91fec52182344",
"shasum": ""
},
"require": {
- "league/uri-interfaces": "^7.7",
+ "league/uri-interfaces": "^7.6",
"php": "^8.1",
"psr/http-factory": "^1"
},
@@ -2816,7 +2752,7 @@
"docs": "https://uri.thephpleague.com",
"forum": "https://thephpleague.slack.com",
"issues": "https://github.com/thephpleague/uri-src/issues",
- "source": "https://github.com/thephpleague/uri/tree/7.7.0"
+ "source": "https://github.com/thephpleague/uri/tree/7.6.0"
},
"funding": [
{
@@ -2824,20 +2760,20 @@
"type": "github"
}
],
- "time": "2025-12-07T16:02:06+00:00"
+ "time": "2025-11-18T12:17:23+00:00"
},
{
"name": "league/uri-interfaces",
- "version": "7.7.0",
+ "version": "7.6.0",
"source": {
"type": "git",
"url": "https://github.com/thephpleague/uri-interfaces.git",
- "reference": "62ccc1a0435e1c54e10ee6022df28d6c04c2946c"
+ "reference": "ccbfb51c0445298e7e0b7f4481b942f589665368"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/thephpleague/uri-interfaces/zipball/62ccc1a0435e1c54e10ee6022df28d6c04c2946c",
- "reference": "62ccc1a0435e1c54e10ee6022df28d6c04c2946c",
+ "url": "https://api.github.com/repos/thephpleague/uri-interfaces/zipball/ccbfb51c0445298e7e0b7f4481b942f589665368",
+ "reference": "ccbfb51c0445298e7e0b7f4481b942f589665368",
"shasum": ""
},
"require": {
@@ -2900,7 +2836,7 @@
"docs": "https://uri.thephpleague.com",
"forum": "https://thephpleague.slack.com",
"issues": "https://github.com/thephpleague/uri-src/issues",
- "source": "https://github.com/thephpleague/uri-interfaces/tree/7.7.0"
+ "source": "https://github.com/thephpleague/uri-interfaces/tree/7.6.0"
},
"funding": [
{
@@ -2908,20 +2844,20 @@
"type": "github"
}
],
- "time": "2025-12-07T16:03:21+00:00"
+ "time": "2025-11-18T12:17:23+00:00"
},
{
"name": "livewire/flux",
- "version": "v2.10.2",
+ "version": "v2.9.2",
"source": {
"type": "git",
"url": "https://github.com/livewire/flux.git",
- "reference": "e7a93989788429bb6c0a908a056d22ea3a6c7975"
+ "reference": "6572847f70a18e7cf136bb31201d4064f5c8ade1"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/livewire/flux/zipball/e7a93989788429bb6c0a908a056d22ea3a6c7975",
- "reference": "e7a93989788429bb6c0a908a056d22ea3a6c7975",
+ "url": "https://api.github.com/repos/livewire/flux/zipball/6572847f70a18e7cf136bb31201d4064f5c8ade1",
+ "reference": "6572847f70a18e7cf136bb31201d4064f5c8ade1",
"shasum": ""
},
"require": {
@@ -2929,12 +2865,12 @@
"illuminate/support": "^10.0|^11.0|^12.0",
"illuminate/view": "^10.0|^11.0|^12.0",
"laravel/prompts": "^0.1|^0.2|^0.3",
- "livewire/livewire": "^3.7.3|^4.0",
+ "livewire/livewire": "^3.5.19|^4.0",
"php": "^8.1",
"symfony/console": "^6.0|^7.0"
},
"conflict": {
- "livewire/blaze": "<1.0.0"
+ "livewire/blaze": "<0.1.0"
},
"type": "library",
"extra": {
@@ -2972,22 +2908,22 @@
],
"support": {
"issues": "https://github.com/livewire/flux/issues",
- "source": "https://github.com/livewire/flux/tree/v2.10.2"
+ "source": "https://github.com/livewire/flux/tree/v2.9.2"
},
- "time": "2025-12-19T02:11:45+00:00"
+ "time": "2025-12-04T17:09:39+00:00"
},
{
"name": "livewire/livewire",
- "version": "v3.7.3",
+ "version": "v3.7.1",
"source": {
"type": "git",
"url": "https://github.com/livewire/livewire.git",
- "reference": "a5384df9fbd3eaf02e053bc49aabc8ace293fc1c"
+ "reference": "214da8f3a1199a88b56ab2fe901d4a607f784805"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/livewire/livewire/zipball/a5384df9fbd3eaf02e053bc49aabc8ace293fc1c",
- "reference": "a5384df9fbd3eaf02e053bc49aabc8ace293fc1c",
+ "url": "https://api.github.com/repos/livewire/livewire/zipball/214da8f3a1199a88b56ab2fe901d4a607f784805",
+ "reference": "214da8f3a1199a88b56ab2fe901d4a607f784805",
"shasum": ""
},
"require": {
@@ -3042,7 +2978,7 @@
"description": "A front-end framework for Laravel.",
"support": {
"issues": "https://github.com/livewire/livewire/issues",
- "source": "https://github.com/livewire/livewire/tree/v3.7.3"
+ "source": "https://github.com/livewire/livewire/tree/v3.7.1"
},
"funding": [
{
@@ -3050,7 +2986,7 @@
"type": "github"
}
],
- "time": "2025-12-19T02:00:29+00:00"
+ "time": "2025-12-03T22:41:13+00:00"
},
{
"name": "livewire/volt",
@@ -3125,16 +3061,16 @@
},
{
"name": "maennchen/zipstream-php",
- "version": "3.2.1",
+ "version": "3.2.0",
"source": {
"type": "git",
"url": "https://github.com/maennchen/ZipStream-PHP.git",
- "reference": "682f1098a8fddbaf43edac2306a691c7ad508ec5"
+ "reference": "9712d8fa4cdf9240380b01eb4be55ad8dcf71416"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/maennchen/ZipStream-PHP/zipball/682f1098a8fddbaf43edac2306a691c7ad508ec5",
- "reference": "682f1098a8fddbaf43edac2306a691c7ad508ec5",
+ "url": "https://api.github.com/repos/maennchen/ZipStream-PHP/zipball/9712d8fa4cdf9240380b01eb4be55ad8dcf71416",
+ "reference": "9712d8fa4cdf9240380b01eb4be55ad8dcf71416",
"shasum": ""
},
"require": {
@@ -3145,7 +3081,7 @@
"require-dev": {
"brianium/paratest": "^7.7",
"ext-zip": "*",
- "friendsofphp/php-cs-fixer": "^3.86",
+ "friendsofphp/php-cs-fixer": "^3.16",
"guzzlehttp/guzzle": "^7.5",
"mikey179/vfsstream": "^1.6",
"php-coveralls/php-coveralls": "^2.5",
@@ -3191,7 +3127,7 @@
],
"support": {
"issues": "https://github.com/maennchen/ZipStream-PHP/issues",
- "source": "https://github.com/maennchen/ZipStream-PHP/tree/3.2.1"
+ "source": "https://github.com/maennchen/ZipStream-PHP/tree/3.2.0"
},
"funding": [
{
@@ -3199,20 +3135,20 @@
"type": "github"
}
],
- "time": "2025-12-10T09:58:31+00:00"
+ "time": "2025-07-17T11:15:13+00:00"
},
{
"name": "monolog/monolog",
- "version": "3.10.0",
+ "version": "3.9.0",
"source": {
"type": "git",
"url": "https://github.com/Seldaek/monolog.git",
- "reference": "b321dd6749f0bf7189444158a3ce785cc16d69b0"
+ "reference": "10d85740180ecba7896c87e06a166e0c95a0e3b6"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/Seldaek/monolog/zipball/b321dd6749f0bf7189444158a3ce785cc16d69b0",
- "reference": "b321dd6749f0bf7189444158a3ce785cc16d69b0",
+ "url": "https://api.github.com/repos/Seldaek/monolog/zipball/10d85740180ecba7896c87e06a166e0c95a0e3b6",
+ "reference": "10d85740180ecba7896c87e06a166e0c95a0e3b6",
"shasum": ""
},
"require": {
@@ -3230,7 +3166,7 @@
"graylog2/gelf-php": "^1.4.2 || ^2.0",
"guzzlehttp/guzzle": "^7.4.5",
"guzzlehttp/psr7": "^2.2",
- "mongodb/mongodb": "^1.8 || ^2.0",
+ "mongodb/mongodb": "^1.8",
"php-amqplib/php-amqplib": "~2.4 || ^3",
"php-console/php-console": "^3.1.8",
"phpstan/phpstan": "^2",
@@ -3290,7 +3226,7 @@
],
"support": {
"issues": "https://github.com/Seldaek/monolog/issues",
- "source": "https://github.com/Seldaek/monolog/tree/3.10.0"
+ "source": "https://github.com/Seldaek/monolog/tree/3.9.0"
},
"funding": [
{
@@ -3302,7 +3238,7 @@
"type": "tidelift"
}
],
- "time": "2026-01-02T08:56:05+00:00"
+ "time": "2025-03-24T10:02:05+00:00"
},
{
"name": "mtdowling/jmespath.php",
@@ -3542,16 +3478,16 @@
},
{
"name": "nette/utils",
- "version": "v4.1.1",
+ "version": "v4.1.0",
"source": {
"type": "git",
"url": "https://github.com/nette/utils.git",
- "reference": "c99059c0315591f1a0db7ad6002000288ab8dc72"
+ "reference": "fa1f0b8261ed150447979eb22e373b7b7ad5a8e0"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/nette/utils/zipball/c99059c0315591f1a0db7ad6002000288ab8dc72",
- "reference": "c99059c0315591f1a0db7ad6002000288ab8dc72",
+ "url": "https://api.github.com/repos/nette/utils/zipball/fa1f0b8261ed150447979eb22e373b7b7ad5a8e0",
+ "reference": "fa1f0b8261ed150447979eb22e373b7b7ad5a8e0",
"shasum": ""
},
"require": {
@@ -3625,22 +3561,22 @@
],
"support": {
"issues": "https://github.com/nette/utils/issues",
- "source": "https://github.com/nette/utils/tree/v4.1.1"
+ "source": "https://github.com/nette/utils/tree/v4.1.0"
},
- "time": "2025-12-22T12:14:32+00:00"
+ "time": "2025-12-01T17:49:23+00:00"
},
{
"name": "nikic/php-parser",
- "version": "v5.7.0",
+ "version": "v5.6.2",
"source": {
"type": "git",
"url": "https://github.com/nikic/PHP-Parser.git",
- "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82"
+ "reference": "3a454ca033b9e06b63282ce19562e892747449bb"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/dca41cd15c2ac9d055ad70dbfd011130757d1f82",
- "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82",
+ "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/3a454ca033b9e06b63282ce19562e892747449bb",
+ "reference": "3a454ca033b9e06b63282ce19562e892747449bb",
"shasum": ""
},
"require": {
@@ -3683,9 +3619,9 @@
],
"support": {
"issues": "https://github.com/nikic/PHP-Parser/issues",
- "source": "https://github.com/nikic/PHP-Parser/tree/v5.7.0"
+ "source": "https://github.com/nikic/PHP-Parser/tree/v5.6.2"
},
- "time": "2025-12-06T11:56:16+00:00"
+ "time": "2025-10-21T19:32:17+00:00"
},
{
"name": "nunomaduro/termwind",
@@ -3776,23 +3712,23 @@
},
{
"name": "om/icalparser",
- "version": "v3.2.1",
+ "version": "v3.2.0",
"source": {
"type": "git",
"url": "https://github.com/OzzyCzech/icalparser.git",
- "reference": "bc7a82b12455ae9b62ce8e7f2d0273e86c931ecc"
+ "reference": "3aa0716aa9e729f08fba20390773d6dcd685169b"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/OzzyCzech/icalparser/zipball/bc7a82b12455ae9b62ce8e7f2d0273e86c931ecc",
- "reference": "bc7a82b12455ae9b62ce8e7f2d0273e86c931ecc",
+ "url": "https://api.github.com/repos/OzzyCzech/icalparser/zipball/3aa0716aa9e729f08fba20390773d6dcd685169b",
+ "reference": "3aa0716aa9e729f08fba20390773d6dcd685169b",
"shasum": ""
},
"require": {
"php": ">=8.1.0"
},
"require-dev": {
- "nette/tester": "^2.5.7"
+ "nette/tester": "^2.5.6"
},
"suggest": {
"ext-dom": "for timezone tool"
@@ -3821,9 +3757,9 @@
],
"support": {
"issues": "https://github.com/OzzyCzech/icalparser/issues",
- "source": "https://github.com/OzzyCzech/icalparser/tree/v3.2.1"
+ "source": "https://github.com/OzzyCzech/icalparser/tree/v3.2.0"
},
- "time": "2025-12-15T06:25:09+00:00"
+ "time": "2025-09-08T07:04:53+00:00"
},
{
"name": "paragonie/constant_time_encoding",
@@ -3946,16 +3882,16 @@
},
{
"name": "phpoption/phpoption",
- "version": "1.9.5",
+ "version": "1.9.4",
"source": {
"type": "git",
"url": "https://github.com/schmittjoh/php-option.git",
- "reference": "75365b91986c2405cf5e1e012c5595cd487a98be"
+ "reference": "638a154f8d4ee6a5cfa96d6a34dfbe0cffa9566d"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/schmittjoh/php-option/zipball/75365b91986c2405cf5e1e012c5595cd487a98be",
- "reference": "75365b91986c2405cf5e1e012c5595cd487a98be",
+ "url": "https://api.github.com/repos/schmittjoh/php-option/zipball/638a154f8d4ee6a5cfa96d6a34dfbe0cffa9566d",
+ "reference": "638a154f8d4ee6a5cfa96d6a34dfbe0cffa9566d",
"shasum": ""
},
"require": {
@@ -4005,7 +3941,7 @@
],
"support": {
"issues": "https://github.com/schmittjoh/php-option/issues",
- "source": "https://github.com/schmittjoh/php-option/tree/1.9.5"
+ "source": "https://github.com/schmittjoh/php-option/tree/1.9.4"
},
"funding": [
{
@@ -4017,20 +3953,20 @@
"type": "tidelift"
}
],
- "time": "2025-12-27T19:41:33+00:00"
+ "time": "2025-08-21T11:53:16+00:00"
},
{
"name": "phpseclib/phpseclib",
- "version": "3.0.48",
+ "version": "3.0.47",
"source": {
"type": "git",
"url": "https://github.com/phpseclib/phpseclib.git",
- "reference": "64065a5679c50acb886e82c07aa139b0f757bb89"
+ "reference": "9d6ca36a6c2dd434765b1071b2644a1c683b385d"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/64065a5679c50acb886e82c07aa139b0f757bb89",
- "reference": "64065a5679c50acb886e82c07aa139b0f757bb89",
+ "url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/9d6ca36a6c2dd434765b1071b2644a1c683b385d",
+ "reference": "9d6ca36a6c2dd434765b1071b2644a1c683b385d",
"shasum": ""
},
"require": {
@@ -4111,7 +4047,7 @@
],
"support": {
"issues": "https://github.com/phpseclib/phpseclib/issues",
- "source": "https://github.com/phpseclib/phpseclib/tree/3.0.48"
+ "source": "https://github.com/phpseclib/phpseclib/tree/3.0.47"
},
"funding": [
{
@@ -4127,7 +4063,7 @@
"type": "tidelift"
}
],
- "time": "2025-12-15T11:51:42+00:00"
+ "time": "2025-10-06T01:07:24+00:00"
},
{
"name": "psr/clock",
@@ -4543,16 +4479,16 @@
},
{
"name": "psy/psysh",
- "version": "v0.12.18",
+ "version": "v0.12.15",
"source": {
"type": "git",
"url": "https://github.com/bobthecow/psysh.git",
- "reference": "ddff0ac01beddc251786fe70367cd8bbdb258196"
+ "reference": "38953bc71491c838fcb6ebcbdc41ab7483cd549c"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/bobthecow/psysh/zipball/ddff0ac01beddc251786fe70367cd8bbdb258196",
- "reference": "ddff0ac01beddc251786fe70367cd8bbdb258196",
+ "url": "https://api.github.com/repos/bobthecow/psysh/zipball/38953bc71491c838fcb6ebcbdc41ab7483cd549c",
+ "reference": "38953bc71491c838fcb6ebcbdc41ab7483cd549c",
"shasum": ""
},
"require": {
@@ -4560,8 +4496,8 @@
"ext-tokenizer": "*",
"nikic/php-parser": "^5.0 || ^4.0",
"php": "^8.0 || ^7.4",
- "symfony/console": "^8.0 || ^7.0 || ^6.0 || ^5.0 || ^4.0 || ^3.4",
- "symfony/var-dumper": "^8.0 || ^7.0 || ^6.0 || ^5.0 || ^4.0 || ^3.4"
+ "symfony/console": "^7.0 || ^6.0 || ^5.0 || ^4.0 || ^3.4",
+ "symfony/var-dumper": "^7.0 || ^6.0 || ^5.0 || ^4.0 || ^3.4"
},
"conflict": {
"symfony/console": "4.4.37 || 5.3.14 || 5.3.15 || 5.4.3 || 5.4.4 || 6.0.3 || 6.0.4"
@@ -4616,9 +4552,9 @@
],
"support": {
"issues": "https://github.com/bobthecow/psysh/issues",
- "source": "https://github.com/bobthecow/psysh/tree/v0.12.18"
+ "source": "https://github.com/bobthecow/psysh/tree/v0.12.15"
},
- "time": "2025-12-17T14:35:46+00:00"
+ "time": "2025-11-28T00:00:14+00:00"
},
{
"name": "ralouphie/getallheaders",
@@ -4742,20 +4678,20 @@
},
{
"name": "ramsey/uuid",
- "version": "4.9.2",
+ "version": "4.9.1",
"source": {
"type": "git",
"url": "https://github.com/ramsey/uuid.git",
- "reference": "8429c78ca35a09f27565311b98101e2826affde0"
+ "reference": "81f941f6f729b1e3ceea61d9d014f8b6c6800440"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/ramsey/uuid/zipball/8429c78ca35a09f27565311b98101e2826affde0",
- "reference": "8429c78ca35a09f27565311b98101e2826affde0",
+ "url": "https://api.github.com/repos/ramsey/uuid/zipball/81f941f6f729b1e3ceea61d9d014f8b6c6800440",
+ "reference": "81f941f6f729b1e3ceea61d9d014f8b6c6800440",
"shasum": ""
},
"require": {
- "brick/math": "^0.8.16 || ^0.9 || ^0.10 || ^0.11 || ^0.12 || ^0.13 || ^0.14",
+ "brick/math": "^0.8.8 || ^0.9 || ^0.10 || ^0.11 || ^0.12 || ^0.13 || ^0.14",
"php": "^8.0",
"ramsey/collection": "^1.2 || ^2.0"
},
@@ -4814,22 +4750,22 @@
],
"support": {
"issues": "https://github.com/ramsey/uuid/issues",
- "source": "https://github.com/ramsey/uuid/tree/4.9.2"
+ "source": "https://github.com/ramsey/uuid/tree/4.9.1"
},
- "time": "2025-12-14T04:43:48+00:00"
+ "time": "2025-09-04T20:59:21+00:00"
},
{
"name": "spatie/browsershot",
- "version": "5.2.0",
+ "version": "5.1.1",
"source": {
"type": "git",
"url": "https://github.com/spatie/browsershot.git",
- "reference": "9bc6b8d67175810d7a399b2588c3401efe2d02a8"
+ "reference": "127c20da43d0d711ebbc64f85053f50bc147c515"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/spatie/browsershot/zipball/9bc6b8d67175810d7a399b2588c3401efe2d02a8",
- "reference": "9bc6b8d67175810d7a399b2588c3401efe2d02a8",
+ "url": "https://api.github.com/repos/spatie/browsershot/zipball/127c20da43d0d711ebbc64f85053f50bc147c515",
+ "reference": "127c20da43d0d711ebbc64f85053f50bc147c515",
"shasum": ""
},
"require": {
@@ -4876,7 +4812,7 @@
"webpage"
],
"support": {
- "source": "https://github.com/spatie/browsershot/tree/5.2.0"
+ "source": "https://github.com/spatie/browsershot/tree/5.1.1"
},
"funding": [
{
@@ -4884,7 +4820,7 @@
"type": "github"
}
],
- "time": "2025-12-22T10:02:16+00:00"
+ "time": "2025-11-26T09:49:20+00:00"
},
{
"name": "spatie/laravel-package-tools",
@@ -5008,72 +4944,6 @@
],
"time": "2025-01-13T13:04:43+00:00"
},
- {
- "name": "stevebauman/purify",
- "version": "v6.3.1",
- "source": {
- "type": "git",
- "url": "https://github.com/stevebauman/purify.git",
- "reference": "3acb5e77904f420ce8aad8fa1c7f394e82daa500"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/stevebauman/purify/zipball/3acb5e77904f420ce8aad8fa1c7f394e82daa500",
- "reference": "3acb5e77904f420ce8aad8fa1c7f394e82daa500",
- "shasum": ""
- },
- "require": {
- "ezyang/htmlpurifier": "^4.17",
- "illuminate/contracts": "^7.0|^8.0|^9.0|^10.0|^11.0|^12.0",
- "illuminate/support": "^7.0|^8.0|^9.0|^10.0|^11.0|^12.0",
- "php": ">=7.4"
- },
- "require-dev": {
- "orchestra/testbench": "^5.0|^6.0|^7.0|^8.0|^9.0|^10.0",
- "phpunit/phpunit": "^8.0|^9.0|^10.0|^11.5.3"
- },
- "type": "library",
- "extra": {
- "laravel": {
- "aliases": {
- "Purify": "Stevebauman\\Purify\\Facades\\Purify"
- },
- "providers": [
- "Stevebauman\\Purify\\PurifyServiceProvider"
- ]
- }
- },
- "autoload": {
- "psr-4": {
- "Stevebauman\\Purify\\": "src/"
- }
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "MIT"
- ],
- "authors": [
- {
- "name": "Steve Bauman",
- "email": "steven_bauman@outlook.com"
- }
- ],
- "description": "An HTML Purifier / Sanitizer for Laravel",
- "keywords": [
- "Purifier",
- "clean",
- "cleaner",
- "html",
- "laravel",
- "purification",
- "purify"
- ],
- "support": {
- "issues": "https://github.com/stevebauman/purify/issues",
- "source": "https://github.com/stevebauman/purify/tree/v6.3.1"
- },
- "time": "2025-05-21T16:53:09+00:00"
- },
{
"name": "symfony/clock",
"version": "v8.0.0",
@@ -5153,16 +5023,16 @@
},
{
"name": "symfony/console",
- "version": "v7.4.3",
+ "version": "v7.4.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/console.git",
- "reference": "732a9ca6cd9dfd940c639062d5edbde2f6727fb6"
+ "reference": "0bc0f45254b99c58d45a8fbf9fb955d46cbd1bb8"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/console/zipball/732a9ca6cd9dfd940c639062d5edbde2f6727fb6",
- "reference": "732a9ca6cd9dfd940c639062d5edbde2f6727fb6",
+ "url": "https://api.github.com/repos/symfony/console/zipball/0bc0f45254b99c58d45a8fbf9fb955d46cbd1bb8",
+ "reference": "0bc0f45254b99c58d45a8fbf9fb955d46cbd1bb8",
"shasum": ""
},
"require": {
@@ -5227,7 +5097,7 @@
"terminal"
],
"support": {
- "source": "https://github.com/symfony/console/tree/v7.4.3"
+ "source": "https://github.com/symfony/console/tree/v7.4.0"
},
"funding": [
{
@@ -5247,24 +5117,24 @@
"type": "tidelift"
}
],
- "time": "2025-12-23T14:50:43+00:00"
+ "time": "2025-11-27T13:27:24+00:00"
},
{
"name": "symfony/css-selector",
- "version": "v8.0.0",
+ "version": "v7.4.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/css-selector.git",
- "reference": "6225bd458c53ecdee056214cb4a2ffaf58bd592b"
+ "reference": "ab862f478513e7ca2fe9ec117a6f01a8da6e1135"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/css-selector/zipball/6225bd458c53ecdee056214cb4a2ffaf58bd592b",
- "reference": "6225bd458c53ecdee056214cb4a2ffaf58bd592b",
+ "url": "https://api.github.com/repos/symfony/css-selector/zipball/ab862f478513e7ca2fe9ec117a6f01a8da6e1135",
+ "reference": "ab862f478513e7ca2fe9ec117a6f01a8da6e1135",
"shasum": ""
},
"require": {
- "php": ">=8.4"
+ "php": ">=8.2"
},
"type": "library",
"autoload": {
@@ -5296,7 +5166,7 @@
"description": "Converts CSS selectors to XPath expressions",
"homepage": "https://symfony.com",
"support": {
- "source": "https://github.com/symfony/css-selector/tree/v8.0.0"
+ "source": "https://github.com/symfony/css-selector/tree/v7.4.0"
},
"funding": [
{
@@ -5316,7 +5186,7 @@
"type": "tidelift"
}
],
- "time": "2025-10-30T14:17:19+00:00"
+ "time": "2025-10-30T13:39:42+00:00"
},
{
"name": "symfony/deprecation-contracts",
@@ -5630,16 +5500,16 @@
},
{
"name": "symfony/filesystem",
- "version": "v8.0.1",
+ "version": "v8.0.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/filesystem.git",
- "reference": "d937d400b980523dc9ee946bb69972b5e619058d"
+ "reference": "7fc96ae83372620eaba3826874f46e26295768ca"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/filesystem/zipball/d937d400b980523dc9ee946bb69972b5e619058d",
- "reference": "d937d400b980523dc9ee946bb69972b5e619058d",
+ "url": "https://api.github.com/repos/symfony/filesystem/zipball/7fc96ae83372620eaba3826874f46e26295768ca",
+ "reference": "7fc96ae83372620eaba3826874f46e26295768ca",
"shasum": ""
},
"require": {
@@ -5676,7 +5546,7 @@
"description": "Provides basic utilities for the filesystem",
"homepage": "https://symfony.com",
"support": {
- "source": "https://github.com/symfony/filesystem/tree/v8.0.1"
+ "source": "https://github.com/symfony/filesystem/tree/v8.0.0"
},
"funding": [
{
@@ -5696,20 +5566,20 @@
"type": "tidelift"
}
],
- "time": "2025-12-01T09:13:36+00:00"
+ "time": "2025-11-05T14:36:47+00:00"
},
{
"name": "symfony/finder",
- "version": "v7.4.3",
+ "version": "v7.4.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/finder.git",
- "reference": "fffe05569336549b20a1be64250b40516d6e8d06"
+ "reference": "340b9ed7320570f319028a2cbec46d40535e94bd"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/finder/zipball/fffe05569336549b20a1be64250b40516d6e8d06",
- "reference": "fffe05569336549b20a1be64250b40516d6e8d06",
+ "url": "https://api.github.com/repos/symfony/finder/zipball/340b9ed7320570f319028a2cbec46d40535e94bd",
+ "reference": "340b9ed7320570f319028a2cbec46d40535e94bd",
"shasum": ""
},
"require": {
@@ -5744,7 +5614,7 @@
"description": "Finds files and directories via an intuitive fluent interface",
"homepage": "https://symfony.com",
"support": {
- "source": "https://github.com/symfony/finder/tree/v7.4.3"
+ "source": "https://github.com/symfony/finder/tree/v7.4.0"
},
"funding": [
{
@@ -5764,20 +5634,20 @@
"type": "tidelift"
}
],
- "time": "2025-12-23T14:50:43+00:00"
+ "time": "2025-11-05T05:42:40+00:00"
},
{
"name": "symfony/http-foundation",
- "version": "v7.4.3",
+ "version": "v7.4.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/http-foundation.git",
- "reference": "a70c745d4cea48dbd609f4075e5f5cbce453bd52"
+ "reference": "769c1720b68e964b13b58529c17d4a385c62167b"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/http-foundation/zipball/a70c745d4cea48dbd609f4075e5f5cbce453bd52",
- "reference": "a70c745d4cea48dbd609f4075e5f5cbce453bd52",
+ "url": "https://api.github.com/repos/symfony/http-foundation/zipball/769c1720b68e964b13b58529c17d4a385c62167b",
+ "reference": "769c1720b68e964b13b58529c17d4a385c62167b",
"shasum": ""
},
"require": {
@@ -5826,7 +5696,7 @@
"description": "Defines an object-oriented layer for the HTTP specification",
"homepage": "https://symfony.com",
"support": {
- "source": "https://github.com/symfony/http-foundation/tree/v7.4.3"
+ "source": "https://github.com/symfony/http-foundation/tree/v7.4.0"
},
"funding": [
{
@@ -5846,20 +5716,20 @@
"type": "tidelift"
}
],
- "time": "2025-12-23T14:23:49+00:00"
+ "time": "2025-11-13T08:49:24+00:00"
},
{
"name": "symfony/http-kernel",
- "version": "v7.4.3",
+ "version": "v7.4.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/http-kernel.git",
- "reference": "885211d4bed3f857b8c964011923528a55702aa5"
+ "reference": "7348193cd384495a755554382e4526f27c456085"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/http-kernel/zipball/885211d4bed3f857b8c964011923528a55702aa5",
- "reference": "885211d4bed3f857b8c964011923528a55702aa5",
+ "url": "https://api.github.com/repos/symfony/http-kernel/zipball/7348193cd384495a755554382e4526f27c456085",
+ "reference": "7348193cd384495a755554382e4526f27c456085",
"shasum": ""
},
"require": {
@@ -5945,7 +5815,7 @@
"description": "Provides a structured process for converting a Request into a Response",
"homepage": "https://symfony.com",
"support": {
- "source": "https://github.com/symfony/http-kernel/tree/v7.4.3"
+ "source": "https://github.com/symfony/http-kernel/tree/v7.4.0"
},
"funding": [
{
@@ -5965,20 +5835,20 @@
"type": "tidelift"
}
],
- "time": "2025-12-31T08:43:57+00:00"
+ "time": "2025-11-27T13:38:24+00:00"
},
{
"name": "symfony/mailer",
- "version": "v7.4.3",
+ "version": "v7.4.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/mailer.git",
- "reference": "e472d35e230108231ccb7f51eb6b2100cac02ee4"
+ "reference": "a3d9eea8cfa467ece41f0f54ba28185d74bd53fd"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/mailer/zipball/e472d35e230108231ccb7f51eb6b2100cac02ee4",
- "reference": "e472d35e230108231ccb7f51eb6b2100cac02ee4",
+ "url": "https://api.github.com/repos/symfony/mailer/zipball/a3d9eea8cfa467ece41f0f54ba28185d74bd53fd",
+ "reference": "a3d9eea8cfa467ece41f0f54ba28185d74bd53fd",
"shasum": ""
},
"require": {
@@ -6029,7 +5899,7 @@
"description": "Helps sending emails",
"homepage": "https://symfony.com",
"support": {
- "source": "https://github.com/symfony/mailer/tree/v7.4.3"
+ "source": "https://github.com/symfony/mailer/tree/v7.4.0"
},
"funding": [
{
@@ -6049,7 +5919,7 @@
"type": "tidelift"
}
],
- "time": "2025-12-16T08:02:06+00:00"
+ "time": "2025-11-21T15:26:00+00:00"
},
{
"name": "symfony/mime",
@@ -6971,16 +6841,16 @@
},
{
"name": "symfony/process",
- "version": "v7.4.3",
+ "version": "v7.4.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/process.git",
- "reference": "2f8e1a6cdf590ca63715da4d3a7a3327404a523f"
+ "reference": "7ca8dc2d0dcf4882658313aba8be5d9fd01026c8"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/process/zipball/2f8e1a6cdf590ca63715da4d3a7a3327404a523f",
- "reference": "2f8e1a6cdf590ca63715da4d3a7a3327404a523f",
+ "url": "https://api.github.com/repos/symfony/process/zipball/7ca8dc2d0dcf4882658313aba8be5d9fd01026c8",
+ "reference": "7ca8dc2d0dcf4882658313aba8be5d9fd01026c8",
"shasum": ""
},
"require": {
@@ -7012,7 +6882,7 @@
"description": "Executes commands in sub-processes",
"homepage": "https://symfony.com",
"support": {
- "source": "https://github.com/symfony/process/tree/v7.4.3"
+ "source": "https://github.com/symfony/process/tree/v7.4.0"
},
"funding": [
{
@@ -7032,20 +6902,20 @@
"type": "tidelift"
}
],
- "time": "2025-12-19T10:00:43+00:00"
+ "time": "2025-10-16T11:21:06+00:00"
},
{
"name": "symfony/routing",
- "version": "v7.4.3",
+ "version": "v7.4.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/routing.git",
- "reference": "5d3fd7adf8896c2fdb54e2f0f35b1bcbd9e45090"
+ "reference": "4720254cb2644a0b876233d258a32bf017330db7"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/routing/zipball/5d3fd7adf8896c2fdb54e2f0f35b1bcbd9e45090",
- "reference": "5d3fd7adf8896c2fdb54e2f0f35b1bcbd9e45090",
+ "url": "https://api.github.com/repos/symfony/routing/zipball/4720254cb2644a0b876233d258a32bf017330db7",
+ "reference": "4720254cb2644a0b876233d258a32bf017330db7",
"shasum": ""
},
"require": {
@@ -7097,7 +6967,7 @@
"url"
],
"support": {
- "source": "https://github.com/symfony/routing/tree/v7.4.3"
+ "source": "https://github.com/symfony/routing/tree/v7.4.0"
},
"funding": [
{
@@ -7117,7 +6987,7 @@
"type": "tidelift"
}
],
- "time": "2025-12-19T10:00:43+00:00"
+ "time": "2025-11-27T13:27:24+00:00"
},
{
"name": "symfony/service-contracts",
@@ -7208,16 +7078,16 @@
},
{
"name": "symfony/string",
- "version": "v8.0.1",
+ "version": "v8.0.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/string.git",
- "reference": "ba65a969ac918ce0cc3edfac6cdde847eba231dc"
+ "reference": "f929eccf09531078c243df72398560e32fa4cf4f"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/string/zipball/ba65a969ac918ce0cc3edfac6cdde847eba231dc",
- "reference": "ba65a969ac918ce0cc3edfac6cdde847eba231dc",
+ "url": "https://api.github.com/repos/symfony/string/zipball/f929eccf09531078c243df72398560e32fa4cf4f",
+ "reference": "f929eccf09531078c243df72398560e32fa4cf4f",
"shasum": ""
},
"require": {
@@ -7274,7 +7144,7 @@
"utf8"
],
"support": {
- "source": "https://github.com/symfony/string/tree/v8.0.1"
+ "source": "https://github.com/symfony/string/tree/v8.0.0"
},
"funding": [
{
@@ -7294,20 +7164,20 @@
"type": "tidelift"
}
],
- "time": "2025-12-01T09:13:36+00:00"
+ "time": "2025-09-11T14:37:55+00:00"
},
{
"name": "symfony/translation",
- "version": "v8.0.3",
+ "version": "v8.0.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/translation.git",
- "reference": "60a8f11f0e15c48f2cc47c4da53873bb5b62135d"
+ "reference": "82ab368a6fca6358d995b6dd5c41590fb42c03e6"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/translation/zipball/60a8f11f0e15c48f2cc47c4da53873bb5b62135d",
- "reference": "60a8f11f0e15c48f2cc47c4da53873bb5b62135d",
+ "url": "https://api.github.com/repos/symfony/translation/zipball/82ab368a6fca6358d995b6dd5c41590fb42c03e6",
+ "reference": "82ab368a6fca6358d995b6dd5c41590fb42c03e6",
"shasum": ""
},
"require": {
@@ -7367,7 +7237,7 @@
"description": "Provides tools to internationalize your application",
"homepage": "https://symfony.com",
"support": {
- "source": "https://github.com/symfony/translation/tree/v8.0.3"
+ "source": "https://github.com/symfony/translation/tree/v8.0.0"
},
"funding": [
{
@@ -7387,7 +7257,7 @@
"type": "tidelift"
}
],
- "time": "2025-12-21T10:59:45+00:00"
+ "time": "2025-11-27T08:09:45+00:00"
},
{
"name": "symfony/translation-contracts",
@@ -7551,16 +7421,16 @@
},
{
"name": "symfony/var-dumper",
- "version": "v7.4.3",
+ "version": "v7.4.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/var-dumper.git",
- "reference": "7e99bebcb3f90d8721890f2963463280848cba92"
+ "reference": "41fd6c4ae28c38b294b42af6db61446594a0dece"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/var-dumper/zipball/7e99bebcb3f90d8721890f2963463280848cba92",
- "reference": "7e99bebcb3f90d8721890f2963463280848cba92",
+ "url": "https://api.github.com/repos/symfony/var-dumper/zipball/41fd6c4ae28c38b294b42af6db61446594a0dece",
+ "reference": "41fd6c4ae28c38b294b42af6db61446594a0dece",
"shasum": ""
},
"require": {
@@ -7614,7 +7484,7 @@
"dump"
],
"support": {
- "source": "https://github.com/symfony/var-dumper/tree/v7.4.3"
+ "source": "https://github.com/symfony/var-dumper/tree/v7.4.0"
},
"funding": [
{
@@ -7634,7 +7504,7 @@
"type": "tidelift"
}
],
- "time": "2025-12-18T07:04:31+00:00"
+ "time": "2025-10-27T20:36:44+00:00"
},
{
"name": "symfony/var-exporter",
@@ -7719,16 +7589,16 @@
},
{
"name": "symfony/yaml",
- "version": "v7.4.1",
+ "version": "v7.4.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/yaml.git",
- "reference": "24dd4de28d2e3988b311751ac49e684d783e2345"
+ "reference": "6c84a4b55aee4cd02034d1c528e83f69ddf63810"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/yaml/zipball/24dd4de28d2e3988b311751ac49e684d783e2345",
- "reference": "24dd4de28d2e3988b311751ac49e684d783e2345",
+ "url": "https://api.github.com/repos/symfony/yaml/zipball/6c84a4b55aee4cd02034d1c528e83f69ddf63810",
+ "reference": "6c84a4b55aee4cd02034d1c528e83f69ddf63810",
"shasum": ""
},
"require": {
@@ -7771,7 +7641,7 @@
"description": "Loads and dumps YAML files",
"homepage": "https://symfony.com",
"support": {
- "source": "https://github.com/symfony/yaml/tree/v7.4.1"
+ "source": "https://github.com/symfony/yaml/tree/v7.4.0"
},
"funding": [
{
@@ -7791,27 +7661,27 @@
"type": "tidelift"
}
],
- "time": "2025-12-04T18:11:45+00:00"
+ "time": "2025-11-16T10:14:42+00:00"
},
{
"name": "tijsverkoyen/css-to-inline-styles",
- "version": "v2.4.0",
+ "version": "v2.3.0",
"source": {
"type": "git",
"url": "https://github.com/tijsverkoyen/CssToInlineStyles.git",
- "reference": "f0292ccf0ec75843d65027214426b6b163b48b41"
+ "reference": "0d72ac1c00084279c1816675284073c5a337c20d"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/tijsverkoyen/CssToInlineStyles/zipball/f0292ccf0ec75843d65027214426b6b163b48b41",
- "reference": "f0292ccf0ec75843d65027214426b6b163b48b41",
+ "url": "https://api.github.com/repos/tijsverkoyen/CssToInlineStyles/zipball/0d72ac1c00084279c1816675284073c5a337c20d",
+ "reference": "0d72ac1c00084279c1816675284073c5a337c20d",
"shasum": ""
},
"require": {
"ext-dom": "*",
"ext-libxml": "*",
"php": "^7.4 || ^8.0",
- "symfony/css-selector": "^5.4 || ^6.0 || ^7.0 || ^8.0"
+ "symfony/css-selector": "^5.4 || ^6.0 || ^7.0"
},
"require-dev": {
"phpstan/phpstan": "^2.0",
@@ -7844,32 +7714,32 @@
"homepage": "https://github.com/tijsverkoyen/CssToInlineStyles",
"support": {
"issues": "https://github.com/tijsverkoyen/CssToInlineStyles/issues",
- "source": "https://github.com/tijsverkoyen/CssToInlineStyles/tree/v2.4.0"
+ "source": "https://github.com/tijsverkoyen/CssToInlineStyles/tree/v2.3.0"
},
- "time": "2025-12-02T11:56:42+00:00"
+ "time": "2024-12-21T16:25:41+00:00"
},
{
"name": "vlucas/phpdotenv",
- "version": "v5.6.3",
+ "version": "v5.6.2",
"source": {
"type": "git",
"url": "https://github.com/vlucas/phpdotenv.git",
- "reference": "955e7815d677a3eaa7075231212f2110983adecc"
+ "reference": "24ac4c74f91ee2c193fa1aaa5c249cb0822809af"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/955e7815d677a3eaa7075231212f2110983adecc",
- "reference": "955e7815d677a3eaa7075231212f2110983adecc",
+ "url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/24ac4c74f91ee2c193fa1aaa5c249cb0822809af",
+ "reference": "24ac4c74f91ee2c193fa1aaa5c249cb0822809af",
"shasum": ""
},
"require": {
"ext-pcre": "*",
- "graham-campbell/result-type": "^1.1.4",
+ "graham-campbell/result-type": "^1.1.3",
"php": "^7.2.5 || ^8.0",
- "phpoption/phpoption": "^1.9.5",
- "symfony/polyfill-ctype": "^1.26",
- "symfony/polyfill-mbstring": "^1.26",
- "symfony/polyfill-php80": "^1.26"
+ "phpoption/phpoption": "^1.9.3",
+ "symfony/polyfill-ctype": "^1.24",
+ "symfony/polyfill-mbstring": "^1.24",
+ "symfony/polyfill-php80": "^1.24"
},
"require-dev": {
"bamarni/composer-bin-plugin": "^1.8.2",
@@ -7918,7 +7788,7 @@
],
"support": {
"issues": "https://github.com/vlucas/phpdotenv/issues",
- "source": "https://github.com/vlucas/phpdotenv/tree/v5.6.3"
+ "source": "https://github.com/vlucas/phpdotenv/tree/v5.6.2"
},
"funding": [
{
@@ -7930,7 +7800,7 @@
"type": "tidelift"
}
],
- "time": "2025-12-27T19:49:13+00:00"
+ "time": "2025-04-30T23:37:27+00:00"
},
{
"name": "voku/portable-ascii",
@@ -8096,16 +7966,16 @@
"packages-dev": [
{
"name": "brianium/paratest",
- "version": "v7.16.1",
+ "version": "v7.15.0",
"source": {
"type": "git",
"url": "https://github.com/paratestphp/paratest.git",
- "reference": "f0fdfd8e654e0d38bc2ba756a6cabe7be287390b"
+ "reference": "272ff9d59b2ed0bd97c86c3cfe97c9784dabf786"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/paratestphp/paratest/zipball/f0fdfd8e654e0d38bc2ba756a6cabe7be287390b",
- "reference": "f0fdfd8e654e0d38bc2ba756a6cabe7be287390b",
+ "url": "https://api.github.com/repos/paratestphp/paratest/zipball/272ff9d59b2ed0bd97c86c3cfe97c9784dabf786",
+ "reference": "272ff9d59b2ed0bd97c86c3cfe97c9784dabf786",
"shasum": ""
},
"require": {
@@ -8116,10 +7986,10 @@
"fidry/cpu-core-counter": "^1.3.0",
"jean85/pretty-package-versions": "^2.1.1",
"php": "~8.3.0 || ~8.4.0 || ~8.5.0",
- "phpunit/php-code-coverage": "^12.5.2",
+ "phpunit/php-code-coverage": "^12.5.0",
"phpunit/php-file-iterator": "^6",
"phpunit/php-timer": "^8",
- "phpunit/phpunit": "^12.5.4",
+ "phpunit/phpunit": "^12.4.4",
"sebastian/environment": "^8.0.3",
"symfony/console": "^7.3.4 || ^8.0.0",
"symfony/process": "^7.3.4 || ^8.0.0"
@@ -8129,9 +7999,9 @@
"ext-pcntl": "*",
"ext-pcov": "*",
"ext-posix": "*",
- "phpstan/phpstan": "^2.1.33",
+ "phpstan/phpstan": "^2.1.32",
"phpstan/phpstan-deprecation-rules": "^2.0.3",
- "phpstan/phpstan-phpunit": "^2.0.11",
+ "phpstan/phpstan-phpunit": "^2.0.8",
"phpstan/phpstan-strict-rules": "^2.0.7",
"symfony/filesystem": "^7.3.2 || ^8.0.0"
},
@@ -8173,7 +8043,7 @@
],
"support": {
"issues": "https://github.com/paratestphp/paratest/issues",
- "source": "https://github.com/paratestphp/paratest/tree/v7.16.1"
+ "source": "https://github.com/paratestphp/paratest/tree/v7.15.0"
},
"funding": [
{
@@ -8185,7 +8055,7 @@
"type": "paypal"
}
],
- "time": "2026-01-08T07:23:06+00:00"
+ "time": "2025-11-30T08:08:11+00:00"
},
{
"name": "doctrine/deprecations",
@@ -8584,16 +8454,16 @@
},
{
"name": "larastan/larastan",
- "version": "v3.8.1",
+ "version": "v3.8.0",
"source": {
"type": "git",
"url": "https://github.com/larastan/larastan.git",
- "reference": "ff3725291bc4c7e6032b5a54776e3e5104c86db9"
+ "reference": "d13ef96d652d1b2a8f34f1760ba6bf5b9c98112e"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/larastan/larastan/zipball/ff3725291bc4c7e6032b5a54776e3e5104c86db9",
- "reference": "ff3725291bc4c7e6032b5a54776e3e5104c86db9",
+ "url": "https://api.github.com/repos/larastan/larastan/zipball/d13ef96d652d1b2a8f34f1760ba6bf5b9c98112e",
+ "reference": "d13ef96d652d1b2a8f34f1760ba6bf5b9c98112e",
"shasum": ""
},
"require": {
@@ -8607,7 +8477,7 @@
"illuminate/pipeline": "^11.44.2 || ^12.4.1",
"illuminate/support": "^11.44.2 || ^12.4.1",
"php": "^8.2",
- "phpstan/phpstan": "^2.1.32"
+ "phpstan/phpstan": "^2.1.29"
},
"require-dev": {
"doctrine/coding-standard": "^13",
@@ -8662,7 +8532,7 @@
],
"support": {
"issues": "https://github.com/larastan/larastan/issues",
- "source": "https://github.com/larastan/larastan/tree/v3.8.1"
+ "source": "https://github.com/larastan/larastan/tree/v3.8.0"
},
"funding": [
{
@@ -8670,38 +8540,38 @@
"type": "github"
}
],
- "time": "2025-12-11T16:37:35+00:00"
+ "time": "2025-10-27T23:09:14+00:00"
},
{
"name": "laravel/boost",
- "version": "v1.8.9",
+ "version": "v1.8.4",
"source": {
"type": "git",
"url": "https://github.com/laravel/boost.git",
- "reference": "1f2c2d41b5216618170fb6730ec13bf894c5bffd"
+ "reference": "dbdef07edbf101049f6d308654ead2f4324de703"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/laravel/boost/zipball/1f2c2d41b5216618170fb6730ec13bf894c5bffd",
- "reference": "1f2c2d41b5216618170fb6730ec13bf894c5bffd",
+ "url": "https://api.github.com/repos/laravel/boost/zipball/dbdef07edbf101049f6d308654ead2f4324de703",
+ "reference": "dbdef07edbf101049f6d308654ead2f4324de703",
"shasum": ""
},
"require": {
"guzzlehttp/guzzle": "^7.9",
- "illuminate/console": "^10.49.0|^11.45.3|^12.41.1",
- "illuminate/contracts": "^10.49.0|^11.45.3|^12.41.1",
- "illuminate/routing": "^10.49.0|^11.45.3|^12.41.1",
- "illuminate/support": "^10.49.0|^11.45.3|^12.41.1",
- "laravel/mcp": "^0.5.1",
+ "illuminate/console": "^10.49.0|^11.45.3|^12.28.1",
+ "illuminate/contracts": "^10.49.0|^11.45.3|^12.28.1",
+ "illuminate/routing": "^10.49.0|^11.45.3|^12.28.1",
+ "illuminate/support": "^10.49.0|^11.45.3|^12.28.1",
+ "laravel/mcp": "^0.4.1",
"laravel/prompts": "0.1.25|^0.3.6",
"laravel/roster": "^0.2.9",
"php": "^8.1"
},
"require-dev": {
- "laravel/pint": "^1.20.0",
+ "laravel/pint": "1.20",
"mockery/mockery": "^1.6.12",
"orchestra/testbench": "^8.36.0|^9.15.0|^10.6",
- "pestphp/pest": "^2.36.0|^3.8.4|^4.1.5",
+ "pestphp/pest": "^2.36.0|^3.8.4",
"phpstan/phpstan": "^2.1.27",
"rector/rector": "^2.1"
},
@@ -8736,33 +8606,33 @@
"issues": "https://github.com/laravel/boost/issues",
"source": "https://github.com/laravel/boost"
},
- "time": "2026-01-07T18:43:11+00:00"
+ "time": "2025-12-05T05:54:57+00:00"
},
{
"name": "laravel/mcp",
- "version": "v0.5.2",
+ "version": "v0.4.1",
"source": {
"type": "git",
"url": "https://github.com/laravel/mcp.git",
- "reference": "b9bdd8d6f8b547c8733fe6826b1819341597ba3c"
+ "reference": "27ab10181d25067de7ace427edb562084d0d0aa3"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/laravel/mcp/zipball/b9bdd8d6f8b547c8733fe6826b1819341597ba3c",
- "reference": "b9bdd8d6f8b547c8733fe6826b1819341597ba3c",
+ "url": "https://api.github.com/repos/laravel/mcp/zipball/27ab10181d25067de7ace427edb562084d0d0aa3",
+ "reference": "27ab10181d25067de7ace427edb562084d0d0aa3",
"shasum": ""
},
"require": {
"ext-json": "*",
"ext-mbstring": "*",
- "illuminate/console": "^10.49.0|^11.45.3|^12.41.1",
- "illuminate/container": "^10.49.0|^11.45.3|^12.41.1",
- "illuminate/contracts": "^10.49.0|^11.45.3|^12.41.1",
- "illuminate/http": "^10.49.0|^11.45.3|^12.41.1",
- "illuminate/json-schema": "^12.41.1",
- "illuminate/routing": "^10.49.0|^11.45.3|^12.41.1",
- "illuminate/support": "^10.49.0|^11.45.3|^12.41.1",
- "illuminate/validation": "^10.49.0|^11.45.3|^12.41.1",
+ "illuminate/console": "^10.49.0|^11.45.3|^12.28.1",
+ "illuminate/container": "^10.49.0|^11.45.3|^12.28.1",
+ "illuminate/contracts": "^10.49.0|^11.45.3|^12.28.1",
+ "illuminate/http": "^10.49.0|^11.45.3|^12.28.1",
+ "illuminate/json-schema": "^12.28.1",
+ "illuminate/routing": "^10.49.0|^11.45.3|^12.28.1",
+ "illuminate/support": "^10.49.0|^11.45.3|^12.28.1",
+ "illuminate/validation": "^10.49.0|^11.45.3|^12.28.1",
"php": "^8.1"
},
"require-dev": {
@@ -8809,7 +8679,7 @@
"issues": "https://github.com/laravel/mcp/issues",
"source": "https://github.com/laravel/mcp"
},
- "time": "2025-12-19T19:32:34+00:00"
+ "time": "2025-12-04T17:29:08+00:00"
},
{
"name": "laravel/pail",
@@ -8892,16 +8762,16 @@
},
{
"name": "laravel/pint",
- "version": "v1.27.0",
+ "version": "v1.26.0",
"source": {
"type": "git",
"url": "https://github.com/laravel/pint.git",
- "reference": "c67b4195b75491e4dfc6b00b1c78b68d86f54c90"
+ "reference": "69dcca060ecb15e4b564af63d1f642c81a241d6f"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/laravel/pint/zipball/c67b4195b75491e4dfc6b00b1c78b68d86f54c90",
- "reference": "c67b4195b75491e4dfc6b00b1c78b68d86f54c90",
+ "url": "https://api.github.com/repos/laravel/pint/zipball/69dcca060ecb15e4b564af63d1f642c81a241d6f",
+ "reference": "69dcca060ecb15e4b564af63d1f642c81a241d6f",
"shasum": ""
},
"require": {
@@ -8912,9 +8782,9 @@
"php": "^8.2.0"
},
"require-dev": {
- "friendsofphp/php-cs-fixer": "^3.92.4",
- "illuminate/view": "^12.44.0",
- "larastan/larastan": "^3.8.1",
+ "friendsofphp/php-cs-fixer": "^3.90.0",
+ "illuminate/view": "^12.40.1",
+ "larastan/larastan": "^3.8.0",
"laravel-zero/framework": "^12.0.4",
"mockery/mockery": "^1.6.12",
"nunomaduro/termwind": "^2.3.3",
@@ -8955,7 +8825,7 @@
"issues": "https://github.com/laravel/pint/issues",
"source": "https://github.com/laravel/pint"
},
- "time": "2026-01-05T16:49:17+00:00"
+ "time": "2025-11-25T21:15:52+00:00"
},
{
"name": "laravel/roster",
@@ -9020,16 +8890,16 @@
},
{
"name": "laravel/sail",
- "version": "v1.52.0",
+ "version": "v1.50.0",
"source": {
"type": "git",
"url": "https://github.com/laravel/sail.git",
- "reference": "64ac7d8abb2dbcf2b76e61289451bae79066b0b3"
+ "reference": "9177d5de1c8247166b92ea6049c2b069d2a1802f"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/laravel/sail/zipball/64ac7d8abb2dbcf2b76e61289451bae79066b0b3",
- "reference": "64ac7d8abb2dbcf2b76e61289451bae79066b0b3",
+ "url": "https://api.github.com/repos/laravel/sail/zipball/9177d5de1c8247166b92ea6049c2b069d2a1802f",
+ "reference": "9177d5de1c8247166b92ea6049c2b069d2a1802f",
"shasum": ""
},
"require": {
@@ -9079,7 +8949,7 @@
"issues": "https://github.com/laravel/sail/issues",
"source": "https://github.com/laravel/sail"
},
- "time": "2026-01-01T02:46:03+00:00"
+ "time": "2025-12-03T17:16:36+00:00"
},
{
"name": "mockery/mockery",
@@ -9325,33 +9195,33 @@
},
{
"name": "pestphp/pest",
- "version": "v4.3.1",
+ "version": "v4.1.6",
"source": {
"type": "git",
"url": "https://github.com/pestphp/pest.git",
- "reference": "bc57a84e77afd4544ff9643a6858f68d05aeab96"
+ "reference": "ae419afd363299c29ad5b17e8b70d118b1068bb4"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/pestphp/pest/zipball/bc57a84e77afd4544ff9643a6858f68d05aeab96",
- "reference": "bc57a84e77afd4544ff9643a6858f68d05aeab96",
+ "url": "https://api.github.com/repos/pestphp/pest/zipball/ae419afd363299c29ad5b17e8b70d118b1068bb4",
+ "reference": "ae419afd363299c29ad5b17e8b70d118b1068bb4",
"shasum": ""
},
"require": {
- "brianium/paratest": "^7.16.0",
+ "brianium/paratest": "^7.14.2",
"nunomaduro/collision": "^8.8.3",
"nunomaduro/termwind": "^2.3.3",
"pestphp/pest-plugin": "^4.0.0",
"pestphp/pest-plugin-arch": "^4.0.0",
"pestphp/pest-plugin-mutate": "^4.0.1",
- "pestphp/pest-plugin-profanity": "^4.2.1",
+ "pestphp/pest-plugin-profanity": "^4.2.0",
"php": "^8.3.0",
- "phpunit/phpunit": "^12.5.4",
- "symfony/process": "^7.4.3|^8.0.0"
+ "phpunit/phpunit": "^12.4.4",
+ "symfony/process": "^7.4.0|^8.0.0"
},
"conflict": {
"filp/whoops": "<2.18.3",
- "phpunit/phpunit": ">12.5.4",
+ "phpunit/phpunit": ">12.4.4",
"sebastian/exporter": "<7.0.0",
"webmozart/assert": "<1.11.0"
},
@@ -9359,7 +9229,7 @@
"pestphp/pest-dev-tools": "^4.0.0",
"pestphp/pest-plugin-browser": "^4.1.1",
"pestphp/pest-plugin-type-coverage": "^4.0.3",
- "psy/psysh": "^0.12.18"
+ "psy/psysh": "^0.12.15"
},
"bin": [
"bin/pest"
@@ -9425,7 +9295,7 @@
],
"support": {
"issues": "https://github.com/pestphp/pest/issues",
- "source": "https://github.com/pestphp/pest/tree/v4.3.1"
+ "source": "https://github.com/pestphp/pest/tree/v4.1.6"
},
"funding": [
{
@@ -9437,7 +9307,7 @@
"type": "github"
}
],
- "time": "2026-01-04T16:29:59+00:00"
+ "time": "2025-11-28T12:04:48+00:00"
},
{
"name": "pestphp/pest-plugin",
@@ -9800,16 +9670,16 @@
},
{
"name": "pestphp/pest-plugin-profanity",
- "version": "v4.2.1",
+ "version": "v4.2.0",
"source": {
"type": "git",
"url": "https://github.com/pestphp/pest-plugin-profanity.git",
- "reference": "343cfa6f3564b7e35df0ebb77b7fa97039f72b27"
+ "reference": "c37e5e2c7136ee4eae12082e7952332bc1c6600a"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/pestphp/pest-plugin-profanity/zipball/343cfa6f3564b7e35df0ebb77b7fa97039f72b27",
- "reference": "343cfa6f3564b7e35df0ebb77b7fa97039f72b27",
+ "url": "https://api.github.com/repos/pestphp/pest-plugin-profanity/zipball/c37e5e2c7136ee4eae12082e7952332bc1c6600a",
+ "reference": "c37e5e2c7136ee4eae12082e7952332bc1c6600a",
"shasum": ""
},
"require": {
@@ -9850,9 +9720,9 @@
"unit"
],
"support": {
- "source": "https://github.com/pestphp/pest-plugin-profanity/tree/v4.2.1"
+ "source": "https://github.com/pestphp/pest-plugin-profanity/tree/v4.2.0"
},
- "time": "2025-12-08T00:13:17+00:00"
+ "time": "2025-10-28T23:14:11+00:00"
},
{
"name": "phar-io/manifest",
@@ -10027,16 +9897,16 @@
},
{
"name": "phpdocumentor/reflection-docblock",
- "version": "5.6.6",
+ "version": "5.6.5",
"source": {
"type": "git",
"url": "https://github.com/phpDocumentor/ReflectionDocBlock.git",
- "reference": "5cee1d3dfc2d2aa6599834520911d246f656bcb8"
+ "reference": "90614c73d3800e187615e2dd236ad0e2a01bf761"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/5cee1d3dfc2d2aa6599834520911d246f656bcb8",
- "reference": "5cee1d3dfc2d2aa6599834520911d246f656bcb8",
+ "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/90614c73d3800e187615e2dd236ad0e2a01bf761",
+ "reference": "90614c73d3800e187615e2dd236ad0e2a01bf761",
"shasum": ""
},
"require": {
@@ -10046,7 +9916,7 @@
"phpdocumentor/reflection-common": "^2.2",
"phpdocumentor/type-resolver": "^1.7",
"phpstan/phpdoc-parser": "^1.7|^2.0",
- "webmozart/assert": "^1.9.1 || ^2"
+ "webmozart/assert": "^1.9.1"
},
"require-dev": {
"mockery/mockery": "~1.3.5 || ~1.6.0",
@@ -10085,9 +9955,9 @@
"description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.",
"support": {
"issues": "https://github.com/phpDocumentor/ReflectionDocBlock/issues",
- "source": "https://github.com/phpDocumentor/ReflectionDocBlock/tree/5.6.6"
+ "source": "https://github.com/phpDocumentor/ReflectionDocBlock/tree/5.6.5"
},
- "time": "2025-12-22T21:13:58+00:00"
+ "time": "2025-11-27T19:50:05+00:00"
},
{
"name": "phpdocumentor/type-resolver",
@@ -10249,23 +10119,23 @@
},
{
"name": "phpunit/php-code-coverage",
- "version": "12.5.2",
+ "version": "12.5.0",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/php-code-coverage.git",
- "reference": "4a9739b51cbcb355f6e95659612f92e282a7077b"
+ "reference": "bca180c050dd3ae15f87c26d25cabb34fe1a0a5a"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/4a9739b51cbcb355f6e95659612f92e282a7077b",
- "reference": "4a9739b51cbcb355f6e95659612f92e282a7077b",
+ "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/bca180c050dd3ae15f87c26d25cabb34fe1a0a5a",
+ "reference": "bca180c050dd3ae15f87c26d25cabb34fe1a0a5a",
"shasum": ""
},
"require": {
"ext-dom": "*",
"ext-libxml": "*",
"ext-xmlwriter": "*",
- "nikic/php-parser": "^5.7.0",
+ "nikic/php-parser": "^5.6.2",
"php": ">=8.3",
"phpunit/php-file-iterator": "^6.0",
"phpunit/php-text-template": "^5.0",
@@ -10273,10 +10143,10 @@
"sebastian/environment": "^8.0.3",
"sebastian/lines-of-code": "^4.0",
"sebastian/version": "^6.0",
- "theseer/tokenizer": "^2.0.1"
+ "theseer/tokenizer": "^1.3.1"
},
"require-dev": {
- "phpunit/phpunit": "^12.5.1"
+ "phpunit/phpunit": "^12.4.4"
},
"suggest": {
"ext-pcov": "PHP extension that provides line coverage",
@@ -10314,7 +10184,7 @@
"support": {
"issues": "https://github.com/sebastianbergmann/php-code-coverage/issues",
"security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy",
- "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/12.5.2"
+ "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/12.5.0"
},
"funding": [
{
@@ -10334,7 +10204,7 @@
"type": "tidelift"
}
],
- "time": "2025-12-24T07:03:04+00:00"
+ "time": "2025-11-29T07:15:54+00:00"
},
{
"name": "phpunit/php-file-iterator",
@@ -10583,16 +10453,16 @@
},
{
"name": "phpunit/phpunit",
- "version": "12.5.4",
+ "version": "12.4.4",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/phpunit.git",
- "reference": "4ba0e923f9d3fc655de22f9547c01d15a41fc93a"
+ "reference": "9253ec75a672e39fcc9d85bdb61448215b8162c7"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/4ba0e923f9d3fc655de22f9547c01d15a41fc93a",
- "reference": "4ba0e923f9d3fc655de22f9547c01d15a41fc93a",
+ "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/9253ec75a672e39fcc9d85bdb61448215b8162c7",
+ "reference": "9253ec75a672e39fcc9d85bdb61448215b8162c7",
"shasum": ""
},
"require": {
@@ -10606,7 +10476,7 @@
"phar-io/manifest": "^2.0.4",
"phar-io/version": "^3.2.1",
"php": ">=8.3",
- "phpunit/php-code-coverage": "^12.5.1",
+ "phpunit/php-code-coverage": "^12.4.0",
"phpunit/php-file-iterator": "^6.0.0",
"phpunit/php-invoker": "^6.0.0",
"phpunit/php-text-template": "^5.0.0",
@@ -10628,7 +10498,7 @@
"type": "library",
"extra": {
"branch-alias": {
- "dev-main": "12.5-dev"
+ "dev-main": "12.4-dev"
}
},
"autoload": {
@@ -10660,7 +10530,7 @@
"support": {
"issues": "https://github.com/sebastianbergmann/phpunit/issues",
"security": "https://github.com/sebastianbergmann/phpunit/security/policy",
- "source": "https://github.com/sebastianbergmann/phpunit/tree/12.5.4"
+ "source": "https://github.com/sebastianbergmann/phpunit/tree/12.4.4"
},
"funding": [
{
@@ -10684,25 +10554,25 @@
"type": "tidelift"
}
],
- "time": "2025-12-15T06:05:34+00:00"
+ "time": "2025-11-21T07:39:11+00:00"
},
{
"name": "rector/rector",
- "version": "2.3.0",
+ "version": "2.2.11",
"source": {
"type": "git",
"url": "https://github.com/rectorphp/rector.git",
- "reference": "f7166355dcf47482f27be59169b0825995f51c7d"
+ "reference": "7bd21a40b0332b93d4bfee284093d7400696902d"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/rectorphp/rector/zipball/f7166355dcf47482f27be59169b0825995f51c7d",
- "reference": "f7166355dcf47482f27be59169b0825995f51c7d",
+ "url": "https://api.github.com/repos/rectorphp/rector/zipball/7bd21a40b0332b93d4bfee284093d7400696902d",
+ "reference": "7bd21a40b0332b93d4bfee284093d7400696902d",
"shasum": ""
},
"require": {
"php": "^7.4|^8.0",
- "phpstan/phpstan": "^2.1.33"
+ "phpstan/phpstan": "^2.1.32"
},
"conflict": {
"rector/rector-doctrine": "*",
@@ -10736,7 +10606,7 @@
],
"support": {
"issues": "https://github.com/rectorphp/rector/issues",
- "source": "https://github.com/rectorphp/rector/tree/2.3.0"
+ "source": "https://github.com/rectorphp/rector/tree/2.2.11"
},
"funding": [
{
@@ -10744,7 +10614,7 @@
"type": "github"
}
],
- "time": "2025-12-25T22:00:18+00:00"
+ "time": "2025-12-02T11:23:46+00:00"
},
{
"name": "sebastian/cli-parser",
@@ -11756,23 +11626,23 @@
},
{
"name": "theseer/tokenizer",
- "version": "2.0.1",
+ "version": "1.3.1",
"source": {
"type": "git",
"url": "https://github.com/theseer/tokenizer.git",
- "reference": "7989e43bf381af0eac72e4f0ca5bcbfa81658be4"
+ "reference": "b7489ce515e168639d17feec34b8847c326b0b3c"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/theseer/tokenizer/zipball/7989e43bf381af0eac72e4f0ca5bcbfa81658be4",
- "reference": "7989e43bf381af0eac72e4f0ca5bcbfa81658be4",
+ "url": "https://api.github.com/repos/theseer/tokenizer/zipball/b7489ce515e168639d17feec34b8847c326b0b3c",
+ "reference": "b7489ce515e168639d17feec34b8847c326b0b3c",
"shasum": ""
},
"require": {
"ext-dom": "*",
"ext-tokenizer": "*",
"ext-xmlwriter": "*",
- "php": "^8.1"
+ "php": "^7.2 || ^8.0"
},
"type": "library",
"autoload": {
@@ -11794,7 +11664,7 @@
"description": "A small library for converting tokenized PHP source code into XML and potentially other formats",
"support": {
"issues": "https://github.com/theseer/tokenizer/issues",
- "source": "https://github.com/theseer/tokenizer/tree/2.0.1"
+ "source": "https://github.com/theseer/tokenizer/tree/1.3.1"
},
"funding": [
{
@@ -11802,27 +11672,27 @@
"type": "github"
}
],
- "time": "2025-12-08T11:19:18+00:00"
+ "time": "2025-11-17T20:03:58+00:00"
},
{
"name": "webmozart/assert",
- "version": "2.1.1",
+ "version": "1.12.1",
"source": {
"type": "git",
"url": "https://github.com/webmozarts/assert.git",
- "reference": "bdbabc199a7ba9965484e4725d66170e5711323b"
+ "reference": "9be6926d8b485f55b9229203f962b51ed377ba68"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/webmozarts/assert/zipball/bdbabc199a7ba9965484e4725d66170e5711323b",
- "reference": "bdbabc199a7ba9965484e4725d66170e5711323b",
+ "url": "https://api.github.com/repos/webmozarts/assert/zipball/9be6926d8b485f55b9229203f962b51ed377ba68",
+ "reference": "9be6926d8b485f55b9229203f962b51ed377ba68",
"shasum": ""
},
"require": {
"ext-ctype": "*",
"ext-date": "*",
"ext-filter": "*",
- "php": "^8.2"
+ "php": "^7.2 || ^8.0"
},
"suggest": {
"ext-intl": "",
@@ -11832,7 +11702,7 @@
"type": "library",
"extra": {
"branch-alias": {
- "dev-feature/2-0": "2.0-dev"
+ "dev-master": "1.10-dev"
}
},
"autoload": {
@@ -11848,10 +11718,6 @@
{
"name": "Bernhard Schussek",
"email": "bschussek@gmail.com"
- },
- {
- "name": "Woody Gilk",
- "email": "woody.gilk@gmail.com"
}
],
"description": "Assertions to validate method input/output with nice error messages.",
@@ -11862,9 +11728,9 @@
],
"support": {
"issues": "https://github.com/webmozarts/assert/issues",
- "source": "https://github.com/webmozarts/assert/tree/2.1.1"
+ "source": "https://github.com/webmozarts/assert/tree/1.12.1"
},
- "time": "2026-01-08T11:28:40+00:00"
+ "time": "2025-10-29T15:56:20+00:00"
}
],
"aliases": [],
diff --git a/config/trustedproxy.php b/config/trustedproxy.php
deleted file mode 100644
index 8557288..0000000
--- a/config/trustedproxy.php
+++ /dev/null
@@ -1,6 +0,0 @@
- ($proxies = env('TRUSTED_PROXIES', '')) === '*' ? '*' : array_filter(explode(',', $proxies)),
-];
diff --git a/database/factories/DevicePaletteFactory.php b/database/factories/DevicePaletteFactory.php
index 1d7ed2d..a672873 100644
--- a/database/factories/DevicePaletteFactory.php
+++ b/database/factories/DevicePaletteFactory.php
@@ -20,7 +20,7 @@ class DevicePaletteFactory extends Factory
public function definition(): array
{
return [
- 'id' => 'test-'.$this->faker->unique()->slug(),
+ 'id' => 'test-' . $this->faker->unique()->slug(),
'name' => $this->faker->words(3, true),
'grays' => $this->faker->randomElement([2, 4, 16, 256]),
'colors' => $this->faker->optional()->passthrough([
diff --git a/database/factories/PluginFactory.php b/database/factories/PluginFactory.php
index 10a1580..a2d2e65 100644
--- a/database/factories/PluginFactory.php
+++ b/database/factories/PluginFactory.php
@@ -29,24 +29,8 @@ class PluginFactory extends Factory
'icon_url' => null,
'flux_icon_name' => null,
'author_name' => $this->faker->name(),
- 'plugin_type' => 'recipe',
'created_at' => Carbon::now(),
'updated_at' => Carbon::now(),
];
}
-
- /**
- * Indicate that the plugin is an image webhook plugin.
- */
- public function imageWebhook(): static
- {
- return $this->state(fn (array $attributes): array => [
- 'plugin_type' => 'image_webhook',
- 'data_strategy' => 'static',
- 'data_stale_minutes' => 60,
- 'polling_url' => null,
- 'polling_verb' => 'get',
- 'name' => $this->faker->randomElement(['Camera Feed', 'Security Camera', 'Webcam', 'Image Stream']),
- ]);
- }
}
diff --git a/database/migrations/2026_01_05_153321_add_plugin_type_to_plugins_table.php b/database/migrations/2026_01_05_153321_add_plugin_type_to_plugins_table.php
deleted file mode 100644
index 558fe2c..0000000
--- a/database/migrations/2026_01_05_153321_add_plugin_type_to_plugins_table.php
+++ /dev/null
@@ -1,28 +0,0 @@
-string('plugin_type')->default('recipe')->after('uuid');
- });
- }
-
- /**
- * Reverse the migrations.
- */
- public function down(): void
- {
- Schema::table('plugins', function (Blueprint $table): void {
- $table->dropColumn('plugin_type');
- });
- }
-};
diff --git a/database/migrations/2026_01_08_173521_add_kind_to_device_models_table.php b/database/migrations/2026_01_08_173521_add_kind_to_device_models_table.php
deleted file mode 100644
index d230657..0000000
--- a/database/migrations/2026_01_08_173521_add_kind_to_device_models_table.php
+++ /dev/null
@@ -1,33 +0,0 @@
-string('kind')->nullable()->index();
- });
-
- // Set existing og_png and og_plus to kind "trmnl"
- DeviceModel::whereIn('name', ['og_png', 'og_plus'])->update(['kind' => 'trmnl']);
- }
-
- /**
- * Reverse the migrations.
- */
- public function down(): void
- {
- Schema::table('device_models', function (Blueprint $table) {
- $table->dropIndex(['kind']);
- $table->dropColumn('kind');
- });
- }
-};
diff --git a/database/migrations/2026_01_11_121809_make_trmnlp_id_unique_in_plugins_table.php b/database/migrations/2026_01_11_121809_make_trmnlp_id_unique_in_plugins_table.php
deleted file mode 100644
index 3b9b1b7..0000000
--- a/database/migrations/2026_01_11_121809_make_trmnlp_id_unique_in_plugins_table.php
+++ /dev/null
@@ -1,58 +0,0 @@
-selectRaw('user_id, trmnlp_id, COUNT(*) as duplicate_count')
- ->whereNotNull('trmnlp_id')
- ->groupBy('user_id', 'trmnlp_id')
- ->havingRaw('COUNT(*) > ?', [1])
- ->get();
-
- // For each duplicate combination, keep the first one (by id) and set others to null
- foreach ($duplicates as $duplicate) {
- $plugins = Plugin::query()
- ->where('user_id', $duplicate->user_id)
- ->where('trmnlp_id', $duplicate->trmnlp_id)
- ->orderBy('id')
- ->get();
-
- // Keep the first one, set the rest to null
- $keepFirst = true;
- foreach ($plugins as $plugin) {
- if ($keepFirst) {
- $keepFirst = false;
-
- continue;
- }
-
- $plugin->update(['trmnlp_id' => null]);
- }
- }
-
- Schema::table('plugins', function (Blueprint $table) {
- $table->unique(['user_id', 'trmnlp_id']);
- });
- }
-
- /**
- * Reverse the migrations.
- */
- public function down(): void
- {
- Schema::table('plugins', function (Blueprint $table) {
- $table->dropUnique(['user_id', 'trmnlp_id']);
- });
- }
-};
diff --git a/database/migrations/2026_01_11_173757_add_alias_to_plugins_table.php b/database/migrations/2026_01_11_173757_add_alias_to_plugins_table.php
deleted file mode 100644
index 0a527d7..0000000
--- a/database/migrations/2026_01_11_173757_add_alias_to_plugins_table.php
+++ /dev/null
@@ -1,28 +0,0 @@
-boolean('alias')->default(false);
- });
- }
-
- /**
- * Reverse the migrations.
- */
- public function down(): void
- {
- Schema::table('plugins', function (Blueprint $table) {
- $table->dropColumn('alias');
- });
- }
-};
diff --git a/database/seeders/ExampleRecipesSeeder.php b/database/seeders/ExampleRecipesSeeder.php
index 890eed9..5474615 100644
--- a/database/seeders/ExampleRecipesSeeder.php
+++ b/database/seeders/ExampleRecipesSeeder.php
@@ -13,8 +13,8 @@ class ExampleRecipesSeeder extends Seeder
public function run($user_id = 1): void
{
Plugin::updateOrCreate(
- ['uuid' => '9e46c6cf-358c-4bfe-8998-436b3a207fec'],
[
+ 'uuid' => '9e46c6cf-358c-4bfe-8998-436b3a207fec',
'name' => 'ÖBB Departures',
'user_id' => $user_id,
'data_payload' => null,
@@ -32,8 +32,8 @@ class ExampleRecipesSeeder extends Seeder
);
Plugin::updateOrCreate(
- ['uuid' => '3b046eda-34e9-4232-b935-c33b989a284b'],
[
+ 'uuid' => '3b046eda-34e9-4232-b935-c33b989a284b',
'name' => 'Weather',
'user_id' => $user_id,
'data_payload' => null,
@@ -51,8 +51,8 @@ class ExampleRecipesSeeder extends Seeder
);
Plugin::updateOrCreate(
- ['uuid' => '21464b16-5f5a-4099-a967-f5c915e3da54'],
[
+ 'uuid' => '21464b16-5f5a-4099-a967-f5c915e3da54',
'name' => 'Zen Quotes',
'user_id' => $user_id,
'data_payload' => null,
@@ -70,8 +70,8 @@ class ExampleRecipesSeeder extends Seeder
);
Plugin::updateOrCreate(
- ['uuid' => '8d472959-400f-46ee-afb2-4a9f1cfd521f'],
[
+ 'uuid' => '8d472959-400f-46ee-afb2-4a9f1cfd521f',
'name' => 'This Day in History',
'user_id' => $user_id,
'data_payload' => null,
@@ -89,8 +89,8 @@ class ExampleRecipesSeeder extends Seeder
);
Plugin::updateOrCreate(
- ['uuid' => '4349fdad-a273-450b-aa00-3d32f2de788d'],
[
+ 'uuid' => '4349fdad-a273-450b-aa00-3d32f2de788d',
'name' => 'Home Assistant',
'user_id' => $user_id,
'data_payload' => null,
@@ -108,8 +108,8 @@ class ExampleRecipesSeeder extends Seeder
);
Plugin::updateOrCreate(
- ['uuid' => 'be5f7e1f-3ad8-4d66-93b2-36f7d6dcbd80'],
[
+ 'uuid' => 'be5f7e1f-3ad8-4d66-93b2-36f7d6dcbd80',
'name' => 'Sunrise/Sunset',
'user_id' => $user_id,
'data_payload' => null,
@@ -127,8 +127,8 @@ class ExampleRecipesSeeder extends Seeder
);
Plugin::updateOrCreate(
- ['uuid' => '82d3ee14-d578-4969-bda5-2bbf825435fe'],
[
+ 'uuid' => '82d3ee14-d578-4969-bda5-2bbf825435fe',
'name' => 'Pollen Forecast',
'user_id' => $user_id,
'data_payload' => null,
@@ -146,8 +146,8 @@ class ExampleRecipesSeeder extends Seeder
);
Plugin::updateOrCreate(
- ['uuid' => '1d98bca4-837d-4b01-b1a1-e3b6e56eca90'],
[
+ 'uuid' => '1d98bca4-837d-4b01-b1a1-e3b6e56eca90',
'name' => 'Holidays (iCal)',
'user_id' => $user_id,
'data_payload' => null,
diff --git a/package-lock.json b/package-lock.json
index e722432..8411d6a 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,5 +1,5 @@
{
- "name": "laravel",
+ "name": "laravel-trmnl-server",
"lockfileVersion": 3,
"requires": true,
"packages": {
@@ -156,6 +156,7 @@
"resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.11.3.tgz",
"integrity": "sha512-9HBM2XnwDj7fnu0551HkGdrUrrqmYq/WC5iv6nbY2WdicXdGbhR/gfbZOH73Aqj4351alY1+aoG9rCNfiwS1RA==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.23.0",
@@ -192,6 +193,7 @@
"resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.5.2.tgz",
"integrity": "sha512-FVqsPqtPWKVVL3dPSxy8wEF/ymIEuVzF1PK3VbUgrxXpJUSHQWWZz4JMToquRxnkw+36LTamCZG2iua2Ptq0fA==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"@marijn/find-cluster-break": "^1.0.0"
}
@@ -213,6 +215,7 @@
"resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.38.8.tgz",
"integrity": "sha512-XcE9fcnkHCbWkjeKyi0lllwXmBLtyYb5dt89dJyx23I9+LSh5vZDIuk7OLG4VM1lgrXZQcY6cxyZyk5WVPRv/A==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"@codemirror/state": "^6.5.0",
"crelt": "^1.0.6",
@@ -715,6 +718,7 @@
"resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.3.tgz",
"integrity": "sha512-qXdH7UqTvGfdVBINrgKhDsVTJTxactNNxLk7+UMwZhU13lMHaOBlJe9Vqp907ya56Y3+ed2tlqzys7jDkTmW0g==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"@lezer/common": "^1.3.0"
}
@@ -1610,6 +1614,7 @@
}
],
"license": "MIT",
+ "peer": true,
"dependencies": {
"baseline-browser-mapping": "^2.9.0",
"caniuse-lite": "^1.0.30001759",
@@ -1893,7 +1898,8 @@
"version": "0.0.1521046",
"resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1521046.tgz",
"integrity": "sha512-vhE6eymDQSKWUXwwA37NtTTVEzjtGVfDr3pRbsWEQ5onH/Snp2c+2xZHWJJawG/0hCCJLRGt4xVtEVUVILol4w==",
- "license": "BSD-3-Clause"
+ "license": "BSD-3-Clause",
+ "peer": true
},
"node_modules/dunder-proto": {
"version": "1.0.1",
@@ -2945,6 +2951,7 @@
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"license": "MIT",
+ "peer": true,
"engines": {
"node": ">=12"
},
@@ -2971,6 +2978,7 @@
}
],
"license": "MIT",
+ "peer": true,
"dependencies": {
"nanoid": "^3.3.11",
"picocolors": "^1.1.1",
@@ -3421,6 +3429,7 @@
"resolved": "https://registry.npmjs.org/vite/-/vite-7.2.6.tgz",
"integrity": "sha512-tI2l/nFHC5rLh7+5+o7QjKjSR04ivXDF4jcgV0f/bTQ+OJiITy5S6gaynVsEM+7RqzufMnVbIon6Sr5x1SDYaQ==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"esbuild": "^0.25.0",
"fdir": "^6.5.0",
diff --git a/public/mirror/assets/apple-touch-icon-120x120.png b/public/mirror/assets/apple-touch-icon-120x120.png
deleted file mode 100644
index 5e51318..0000000
Binary files a/public/mirror/assets/apple-touch-icon-120x120.png and /dev/null differ
diff --git a/public/mirror/assets/apple-touch-icon-152x152.png b/public/mirror/assets/apple-touch-icon-152x152.png
deleted file mode 100644
index 9f8d9e3..0000000
Binary files a/public/mirror/assets/apple-touch-icon-152x152.png and /dev/null differ
diff --git a/public/mirror/assets/apple-touch-icon-167x167.png b/public/mirror/assets/apple-touch-icon-167x167.png
deleted file mode 100644
index 79d1211..0000000
Binary files a/public/mirror/assets/apple-touch-icon-167x167.png and /dev/null differ
diff --git a/public/mirror/assets/apple-touch-icon-180x180.png b/public/mirror/assets/apple-touch-icon-180x180.png
deleted file mode 100644
index 0499ff4..0000000
Binary files a/public/mirror/assets/apple-touch-icon-180x180.png and /dev/null differ
diff --git a/public/mirror/assets/apple-touch-icon-76x76.png b/public/mirror/assets/apple-touch-icon-76x76.png
deleted file mode 100644
index df3943a..0000000
Binary files a/public/mirror/assets/apple-touch-icon-76x76.png and /dev/null differ
diff --git a/public/mirror/assets/favicon-16x16.png b/public/mirror/assets/favicon-16x16.png
deleted file mode 100644
index b36f23b..0000000
Binary files a/public/mirror/assets/favicon-16x16.png and /dev/null differ
diff --git a/public/mirror/assets/favicon-32x32.png b/public/mirror/assets/favicon-32x32.png
deleted file mode 100644
index ae12e60..0000000
Binary files a/public/mirror/assets/favicon-32x32.png and /dev/null differ
diff --git a/public/mirror/assets/favicon.ico b/public/mirror/assets/favicon.ico
deleted file mode 100644
index da17cd5..0000000
Binary files a/public/mirror/assets/favicon.ico and /dev/null differ
diff --git a/public/mirror/assets/logo--brand.svg b/public/mirror/assets/logo--brand.svg
deleted file mode 100644
index 1b84f50..0000000
--- a/public/mirror/assets/logo--brand.svg
+++ /dev/null
@@ -1,139 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/public/mirror/index.html b/public/mirror/index.html
deleted file mode 100644
index 64746fe..0000000
--- a/public/mirror/index.html
+++ /dev/null
@@ -1,521 +0,0 @@
-
-
-
-
-
- TRMNL BYOS Laravel Mirror
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/public/mirror/manifest.json b/public/mirror/manifest.json
deleted file mode 100644
index 4d44e44..0000000
--- a/public/mirror/manifest.json
+++ /dev/null
@@ -1,7 +0,0 @@
-{
- "name": "TRMNL BYOS Laravel Mirror",
- "short_name": "TRMNL BYOS",
- "display": "standalone",
- "background_color": "#ffffff",
- "theme_color": "#ffffff"
-}
diff --git a/resources/css/app.css b/resources/css/app.css
index de95b81..46b9ca1 100644
--- a/resources/css/app.css
+++ b/resources/css/app.css
@@ -59,10 +59,6 @@
@apply !mb-0 !leading-tight;
}
-[data-flux-description] a {
- @apply text-accent underline hover:opacity-80;
-}
-
input:focus[data-flux-control],
textarea:focus[data-flux-control],
select:focus[data-flux-control] {
@@ -72,39 +68,3 @@ select:focus[data-flux-control] {
/* \[:where(&)\]:size-4 {
@apply size-4;
} */
-
-@layer components {
- /* standard container for app */
- .styled-container,
- .tab-button {
- @apply rounded-xl border bg-white dark:bg-stone-950 dark:border-zinc-700 text-stone-800 shadow-xs;
- }
-
- .tab-button {
- @apply flex items-center gap-2 px-4 py-2 text-sm font-medium;
- @apply rounded-b-none shadow-none bg-inherit;
-
- /* This makes the button sit slightly over the box border */
- margin-bottom: -1px;
- position: relative;
- z-index: 1;
- }
-
- .tab-button.is-active {
- @apply text-zinc-700 dark:text-zinc-300;
- @apply border-b-white dark:border-b-zinc-800;
-
- /* Z-index 10 ensures the bottom border of the tab hides the top border of the box */
- z-index: 10;
- }
-
- .tab-button:not(.is-active) {
- @apply text-zinc-500 border-transparent;
- }
-
- .tab-button:not(.is-active):hover {
- @apply text-zinc-700 dark:text-zinc-300;
- @apply border-zinc-300 dark:border-zinc-700;
- cursor: pointer;
- }
-}
diff --git a/resources/js/codemirror-core.js b/resources/js/codemirror-core.js
index f23389f..c77bf3d 100644
--- a/resources/js/codemirror-core.js
+++ b/resources/js/codemirror-core.js
@@ -1,9 +1,8 @@
import { EditorView, lineNumbers, keymap } from '@codemirror/view';
import { ViewPlugin } from '@codemirror/view';
-import { indentWithTab, selectAll } from '@codemirror/commands';
+import { indentWithTab } from '@codemirror/commands';
import { foldGutter, foldKeymap } from '@codemirror/language';
import { history, historyKeymap } from '@codemirror/commands';
-import { searchKeymap } from '@codemirror/search';
import { html } from '@codemirror/lang-html';
import { javascript } from '@codemirror/lang-javascript';
import { json } from '@codemirror/lang-json';
@@ -155,16 +154,7 @@ export function createCodeMirror(element, options = {}) {
createResizePlugin(),
...(Array.isArray(languageSupport) ? languageSupport : [languageSupport]),
...themeSupport,
- keymap.of([
- indentWithTab,
- ...foldKeymap,
- ...historyKeymap,
- ...searchKeymap,
- {
- key: 'Mod-a',
- run: selectAll,
- },
- ]),
+ keymap.of([indentWithTab, ...foldKeymap, ...historyKeymap]),
EditorView.theme({
'&': {
fontSize: '14px',
diff --git a/resources/views/components/layouts/auth/card.blade.php b/resources/views/components/layouts/auth/card.blade.php
index b5a62c6..1a316ef 100644
--- a/resources/views/components/layouts/auth/card.blade.php
+++ b/resources/views/components/layouts/auth/card.blade.php
@@ -15,7 +15,7 @@
-
diff --git a/resources/views/default-screens/error.blade.php b/resources/views/default-screens/error.blade.php
deleted file mode 100644
index be8063a..0000000
--- a/resources/views/default-screens/error.blade.php
+++ /dev/null
@@ -1,23 +0,0 @@
-@props([
- 'noBleed' => false,
- 'darkMode' => false,
- 'deviceVariant' => 'og',
- 'deviceOrientation' => null,
- 'colorDepth' => '1bit',
- 'scaleLevel' => null,
- 'pluginName' => 'Recipe',
-])
-
-
-
-
-
- Error on {{ $pluginName }}
- Unable to render content. Please check server logs.
-
-
-
-
-
diff --git a/resources/views/livewire/catalog/index.blade.php b/resources/views/livewire/catalog/index.blade.php
index fdf7f34..201ee7e 100644
--- a/resources/views/livewire/catalog/index.blade.php
+++ b/resources/views/livewire/catalog/index.blade.php
@@ -1,26 +1,20 @@
loadCatalogPlugins();
@@ -55,7 +49,7 @@ class extends Component
return collect($catalog)
->filter(function ($plugin) use ($currentVersion) {
// Check if Laravel compatibility is true
- if (! Arr::get($plugin, 'byos.byos_laravel.compatibility', false)) {
+ if (!Arr::get($plugin, 'byos.byos_laravel.compatibility', false)) {
return false;
}
@@ -85,9 +79,8 @@ class extends Component
})
->sortBy('name')
->toArray();
- } catch (Exception $e) {
- Log::error('Failed to load catalog from URL: '.$e->getMessage());
-
+ } catch (\Exception $e) {
+ Log::error('Failed to load catalog from URL: ' . $e->getMessage());
return [];
}
});
@@ -99,9 +92,8 @@ class extends Component
$plugin = collect($this->catalogPlugins)->firstWhere('id', $pluginId);
- if (! $plugin || ! $plugin['zip_url']) {
+ if (!$plugin || !$plugin['zip_url']) {
$this->addError('installation', 'Plugin not found or no download URL available.');
-
return;
}
@@ -113,45 +105,24 @@ class extends Component
auth()->user(),
$plugin['zip_entry_path'] ?? null,
null,
- $plugin['logo_url'] ?? null,
- allowDuplicate: true
+ $plugin['logo_url'] ?? null
);
$this->dispatch('plugin-installed');
Flux::modal('import-from-catalog')->close();
- } catch (Exception $e) {
- $this->addError('installation', 'Error installing plugin: '.$e->getMessage());
+ } catch (\Exception $e) {
+ $this->addError('installation', 'Error installing plugin: ' . $e->getMessage());
} finally {
$this->installingPlugin = '';
}
}
-
- public function previewPlugin(string $pluginId): void
- {
- $plugin = collect($this->catalogPlugins)->firstWhere('id', $pluginId);
-
- if (! $plugin) {
- $this->addError('preview', 'Plugin not found.');
-
- return;
- }
-
- $this->previewingPlugin = $pluginId;
- $this->previewData = $plugin;
- }
-
- public function closePreview(): void
- {
- $this->previewingPlugin = '';
- $this->previewData = [];
- }
}; ?>
@if(empty($catalogPlugins))
-
+
No plugins available
Catalog is empty
@@ -162,30 +133,30 @@ class extends Component
@enderror
@foreach($catalogPlugins as $plugin)
-
+
@if($plugin['logo_url'])
@else
-
-
+
+
@endif
-
{{ $plugin['name'] }}
+
{{ $plugin['name'] }}
@if ($plugin['github'])
-
by {{ $plugin['github'] }}
+
by {{ $plugin['github'] }}
@endif
@if($plugin['license'])
-
{{ $plugin['license'] }}
+
{{ $plugin['license'] }}
@endif
@if($plugin['repo_url'])
-
+
@endif
@@ -193,7 +164,7 @@ class extends Component
@if($plugin['description'])
-
{{ $plugin['description'] }}
+
{{ $plugin['description'] }}
@endif
@@ -203,19 +174,6 @@ class extends Component
Install
- @if($plugin['screenshot_url'])
-
-
- Preview
-
-
- @endif
-
-
-
@if($plugin['learn_more_url'])
@endif
-
-
-
- @if($previewingPlugin && !empty($previewData))
-
- Preview {{ $previewData['name'] ?? 'Plugin' }}
-
-
-
-
-
-
-
- @if($previewData['description'])
-
- Description
- {{ $previewData['description'] }}
-
- @endif
-
-
-
-
- Install Plugin
-
-
-
-
- @endif
-
diff --git a/resources/views/livewire/catalog/trmnl.blade.php b/resources/views/livewire/catalog/trmnl.blade.php
index cc8b070..8e9c7af 100644
--- a/resources/views/livewire/catalog/trmnl.blade.php
+++ b/resources/views/livewire/catalog/trmnl.blade.php
@@ -1,30 +1,20 @@
loadNewest();
@@ -47,36 +37,22 @@ class extends Component
private function loadNewest(): void
{
try {
- $cacheKey = 'trmnl_recipes_newest_page_'.$this->page;
- $response = Cache::remember($cacheKey, 43200, function () {
+ $this->recipes = Cache::remember('trmnl_recipes_newest', 43200, function () {
$response = Http::timeout(10)->get('https://usetrmnl.com/recipes.json', [
'sort-by' => 'newest',
- 'page' => $this->page,
]);
- if (! $response->successful()) {
- throw new RuntimeException('Failed to fetch TRMNL recipes');
+ if (!$response->successful()) {
+ throw new \RuntimeException('Failed to fetch TRMNL recipes');
}
- return $response->json();
+ $json = $response->json();
+ $data = $json['data'] ?? [];
+ return $this->mapRecipes($data);
});
-
- $data = $response['data'] ?? [];
- $mapped = $this->mapRecipes($data);
-
- if ($this->page === 1) {
- $this->recipes = $mapped;
- } else {
- $this->recipes = array_merge($this->recipes, $mapped);
- }
-
- $this->hasMore = ! empty($response['next_page_url']);
- } catch (Throwable $e) {
- Log::error('TRMNL catalog load error: '.$e->getMessage());
- if ($this->page === 1) {
- $this->recipes = [];
- }
- $this->hasMore = false;
+ } catch (\Throwable $e) {
+ Log::error('TRMNL catalog load error: ' . $e->getMessage());
+ $this->recipes = [];
}
}
@@ -84,65 +60,38 @@ class extends Component
{
$this->isSearching = true;
try {
- $cacheKey = 'trmnl_recipes_search_'.md5($term).'_page_'.$this->page;
- $response = Cache::remember($cacheKey, 300, function () use ($term) {
+ $cacheKey = 'trmnl_recipes_search_' . md5($term);
+ $this->recipes = Cache::remember($cacheKey, 300, function () use ($term) {
$response = Http::get('https://usetrmnl.com/recipes.json', [
'search' => $term,
'sort-by' => 'newest',
- 'page' => $this->page,
]);
- if (! $response->successful()) {
- throw new RuntimeException('Failed to search TRMNL recipes');
+ if (!$response->successful()) {
+ throw new \RuntimeException('Failed to search TRMNL recipes');
}
- return $response->json();
+ $json = $response->json();
+ $data = $json['data'] ?? [];
+ return $this->mapRecipes($data);
});
-
- $data = $response['data'] ?? [];
- $mapped = $this->mapRecipes($data);
-
- if ($this->page === 1) {
- $this->recipes = $mapped;
- } else {
- $this->recipes = array_merge($this->recipes, $mapped);
- }
-
- $this->hasMore = ! empty($response['next_page_url']);
- } catch (Throwable $e) {
- Log::error('TRMNL catalog search error: '.$e->getMessage());
- if ($this->page === 1) {
- $this->recipes = [];
- }
- $this->hasMore = false;
+ } catch (\Throwable $e) {
+ Log::error('TRMNL catalog search error: ' . $e->getMessage());
+ $this->recipes = [];
} finally {
$this->isSearching = false;
}
}
- public function loadMore(): void
- {
- $this->page++;
-
- $term = mb_trim($this->search);
- if ($term === '' || mb_strlen($term) < 2) {
- $this->loadNewest();
- } else {
- $this->searchRecipes($term);
- }
- }
-
public function updatedSearch(): void
{
- $this->page = 1;
- $term = mb_trim($this->search);
+ $term = trim($this->search);
if ($term === '') {
$this->loadNewest();
-
return;
}
- if (mb_strlen($term) < 2) {
+ if (strlen($term) < 2) {
// Require at least 2 chars to avoid noisy calls
return;
}
@@ -164,85 +113,43 @@ class extends Component
auth()->user(),
null,
config('services.trmnl.liquid_enabled') ? 'trmnl-liquid' : null,
- $recipe['icon_url'] ?? null,
- allowDuplicate: true
+ $recipe['icon_url'] ?? null
);
$this->dispatch('plugin-installed');
Flux::modal('import-from-trmnl-catalog')->close();
- } catch (Exception $e) {
- Log::error('Plugin installation failed: '.$e->getMessage());
- $this->addError('installation', 'Error installing plugin: '.$e->getMessage());
- }
- }
-
- public function previewRecipe(string $recipeId): void
- {
- $this->previewingRecipe = $recipeId;
- $this->previewData = [];
-
- try {
- $response = Http::timeout(10)->get("https://usetrmnl.com/recipes/{$recipeId}.json");
-
- if ($response->successful()) {
- $item = $response->json()['data'] ?? [];
- $this->previewData = $this->mapRecipe($item);
- } else {
- // Fallback to searching for the specific recipe if single endpoint doesn't exist
- $response = Http::timeout(10)->get('https://usetrmnl.com/recipes.json', [
- 'search' => $recipeId,
- ]);
-
- if ($response->successful()) {
- $data = $response->json()['data'] ?? [];
- $item = collect($data)->firstWhere('id', $recipeId);
- if ($item) {
- $this->previewData = $this->mapRecipe($item);
- }
- }
- }
- } catch (Throwable $e) {
- Log::error('TRMNL catalog preview fetch error: '.$e->getMessage());
- }
-
- if (empty($this->previewData)) {
- $this->previewData = collect($this->recipes)->firstWhere('id', $recipeId) ?? [];
+ } catch (\Exception $e) {
+ Log::error('Plugin installation failed: ' . $e->getMessage());
+ $this->addError('installation', 'Error installing plugin: ' . $e->getMessage());
}
}
/**
- * @param array
> $items
+ * @param array> $items
* @return array>
*/
private function mapRecipes(array $items): array
{
return collect($items)
- ->map(fn (array $item) => $this->mapRecipe($item))
+ ->map(function (array $item) {
+ return [
+ 'id' => $item['id'] ?? null,
+ 'name' => $item['name'] ?? 'Untitled',
+ 'icon_url' => $item['icon_url'] ?? null,
+ 'screenshot_url' => $item['screenshot_url'] ?? null,
+ 'author_bio' => is_array($item['author_bio'] ?? null)
+ ? strip_tags($item['author_bio']['description'] ?? null)
+ : null,
+ 'stats' => [
+ 'installs' => data_get($item, 'stats.installs'),
+ 'forks' => data_get($item, 'stats.forks'),
+ ],
+ 'detail_url' => isset($item['id']) ? ('https://usetrmnl.com/recipes/' . $item['id']) : null,
+ ];
+ })
->toArray();
}
-
- /**
- * @param array $item
- * @return array
- */
- private function mapRecipe(array $item): array
- {
- return [
- 'id' => $item['id'] ?? null,
- 'name' => $item['name'] ?? 'Untitled',
- 'icon_url' => $item['icon_url'] ?? null,
- 'screenshot_url' => $item['screenshot_url'] ?? null,
- 'author_bio' => is_array($item['author_bio'] ?? null)
- ? strip_tags($item['author_bio']['description'] ?? null)
- : null,
- 'stats' => [
- 'installs' => data_get($item, 'stats.installs'),
- 'forks' => data_get($item, 'stats.forks'),
- ],
- 'detail_url' => isset($item['id']) ? ('https://usetrmnl.com/recipes/'.$item['id']) : null,
- ];
- }
}; ?>
@@ -254,7 +161,7 @@ class extends Component
icon="magnifying-glass"
/>
- Newest
+ Newest
@error('installation')
@@ -263,36 +170,35 @@ class extends Component
@if(empty($recipes))
-
+
No recipes found
Try a different search term
@else
@foreach($recipes as $recipe)
-
-
-
+
+
@php($thumb = $recipe['icon_url'] ?? $recipe['screenshot_url'])
@if($thumb)
@else
-
-
+
+
@endif
-
{{ $recipe['name'] }}
+
{{ $recipe['name'] }}
@if(data_get($recipe, 'stats.installs'))
-
Installs: {{ data_get($recipe, 'stats.installs') }} · Forks: {{ data_get($recipe, 'stats.forks') }}
+
Installs: {{ data_get($recipe, 'stats.installs') }} · Forks: {{ data_get($recipe, 'stats.forks') }}
@endif
@if($recipe['detail_url'])
-
+
@endif
@@ -300,7 +206,7 @@ class extends Component
@if($recipe['author_bio'])
-
{{ $recipe['author_bio'] }}
+
{{ $recipe['author_bio'] }}
@endif
@@ -312,96 +218,19 @@ class extends Component
@endif
- @if($recipe['id'] && ($recipe['screenshot_url'] ?? null))
-
-
- Preview
-
-
+ @if($recipe['detail_url'])
+
+ View on TRMNL
+
@endif
-
@endforeach
-
- @if($hasMore)
-
-
- Load next page
- Loading...
-
-
- @endif
@endif
-
-
-
-
-
-
- Fetching recipe details...
-
-
-
-
- @if($previewingRecipe && !empty($previewData))
-
- Preview {{ $previewData['name'] ?? 'Recipe' }}
-
-
-
-
-
-
-
- @if($previewData['author_bio'])
-
-
- Description
- {{ $previewData['author_bio'] }}
-
-
- @endif
-
- @if(data_get($previewData, 'stats.installs'))
-
-
- Statistics
-
- Installs: {{ data_get($previewData, 'stats.installs') }} ·
- Forks: {{ data_get($previewData, 'stats.forks') }}
-
-
-
- @endif
-
-
- @if($previewData['detail_url'])
-
- View on TRMNL
-
- @endif
-
-
- Install Recipe
-
-
-
-
- @endif
-
-
diff --git a/resources/views/livewire/device-dashboard.blade.php b/resources/views/livewire/device-dashboard.blade.php
index 7fd48a8..5db65d1 100644
--- a/resources/views/livewire/device-dashboard.blade.php
+++ b/resources/views/livewire/device-dashboard.blade.php
@@ -16,7 +16,7 @@ new class extends Component {
@if($devices->isEmpty())
+ class="rounded-xl border bg-white dark:bg-stone-950 dark:border-stone-800 text-stone-800 shadow-xs">
Add your first device
+ class="rounded-xl border bg-white dark:bg-stone-950 dark:border-stone-800 text-stone-800 shadow-xs">
@php
$current_image_uuid =$device->current_screen_image;
diff --git a/resources/views/livewire/devices/configure.blade.php b/resources/views/livewire/devices/configure.blade.php
index f9d49ca..30b4481 100644
--- a/resources/views/livewire/devices/configure.blade.php
+++ b/resources/views/livewire/devices/configure.blade.php
@@ -309,7 +309,7 @@ new class extends Component {
+ class="rounded-xl border bg-white dark:bg-stone-950 dark:border-stone-800 text-stone-800 shadow-xs">
@php
$current_image_uuid =$device->current_screen_image;
@@ -368,10 +368,6 @@ new class extends Component {
Update Firmware
Show Logs
-
- Mirror URL
-
-
Delete Device
@@ -502,26 +498,6 @@ new class extends Component {
-
- @php
- $mirrorUrl = url('/mirror/index.html') . '?mac_address=' . urlencode($device->mac_address) . '&api_key=' . urlencode($device->api_key);
- @endphp
-
-
-
- Mirror WebUI
- Mirror this device onto older devices with a web browser — Safari is supported back to iOS 9.
-
-
-
-
-
-
@if(!$device->mirror_device_id)
@if($current_image_path)
diff --git a/resources/views/livewire/playlists/index.blade.php b/resources/views/livewire/playlists/index.blade.php
index 6c979e6..3e786b4 100644
--- a/resources/views/livewire/playlists/index.blade.php
+++ b/resources/views/livewire/playlists/index.blade.php
@@ -332,7 +332,7 @@ new class extends Component {
@endforeach
@if($devices->isEmpty() || $devices->every(fn($device) => $device->playlists->isEmpty()))
-
+
No playlists found
Add playlists to your devices to see them here.
diff --git a/resources/views/livewire/plugins/config-modal.blade.php b/resources/views/livewire/plugins/config-modal.blade.php
deleted file mode 100644
index 7aaacbb..0000000
--- a/resources/views/livewire/plugins/config-modal.blade.php
+++ /dev/null
@@ -1,516 +0,0 @@
- loadData();
- }
-
- public function loadData(): void
- {
- $this->resetErrorBag();
- // Reload data
- $this->plugin = $this->plugin->fresh();
-
- $this->configuration_template = $this->plugin->configuration_template ?? [];
- $this->configuration = is_array($this->plugin->configuration) ? $this->plugin->configuration : [];
-
- // Initialize multiValues by exploding the CSV strings from the DB
- foreach ($this->configuration_template['custom_fields'] ?? [] as $field) {
- if (($field['field_type'] ?? null) === 'multi_string') {
- $fieldKey = $field['keyname'];
- $rawValue = $this->configuration[$fieldKey] ?? ($field['default'] ?? '');
-
- $currentValue = is_array($rawValue) ? '' : (string)$rawValue;
-
- $this->multiValues[$fieldKey] = $currentValue !== ''
- ? array_values(array_filter(explode(',', $currentValue)))
- : [''];
- }
- }
- }
-
- /**
- * Triggered by @close on the modal to discard any typed but unsaved changes
- */
- public int $resetIndex = 0; // Add this property
- public function resetForm(): void
- {
- $this->loadData();
- $this->resetIndex++; // Increment to force DOM refresh
- }
-
- public function saveConfiguration()
- {
- abort_unless(auth()->user()->plugins->contains($this->plugin), 403);
-
- // final validation layer
- $this->validate([
- 'multiValues.*.*' => ['nullable', 'string', 'regex:/^[^,]*$/'],
- ], [
- 'multiValues.*.*.regex' => 'Items cannot contain commas.',
- ]);
-
- // Prepare config copy to send to db
- $finalValues = $this->configuration;
- foreach ($this->configuration_template['custom_fields'] ?? [] as $field) {
- $fieldKey = $field['keyname'];
-
- // Handle multi_string: Join array back to CSV string
- if ($field['field_type'] === 'multi_string' && isset($this->multiValues[$fieldKey])) {
- $finalValues[$fieldKey] = implode(',', array_filter(array_map('trim', $this->multiValues[$fieldKey])));
- }
-
- // Handle code fields: Try to JSON decode if necessary (standard TRMNL behavior)
- if ($field['field_type'] === 'code' && isset($finalValues[$fieldKey]) && is_string($finalValues[$fieldKey])) {
- $decoded = json_decode($finalValues[$fieldKey], true);
- if (json_last_error() === JSON_ERROR_NONE && (is_array($decoded) || is_object($decoded))) {
- $finalValues[$fieldKey] = $decoded;
- }
- }
- }
-
- // send to db
- $this->plugin->update(['configuration' => $finalValues]);
- $this->configuration = $finalValues; // update local state
- $this->dispatch('config-updated'); // notifies listeners
- Flux::modal('configuration-modal')->close();
- }
-
- // ------------------------------------This section contains helper functions for interacting with the form------------------------------------------------
- public function addMultiItem(string $fieldKey): void
- {
- $this->multiValues[$fieldKey][] = '';
- }
-
- public function removeMultiItem(string $fieldKey, int $index): void
- {
- unset($this->multiValues[$fieldKey][$index]);
-
- $this->multiValues[$fieldKey] = array_values($this->multiValues[$fieldKey]);
-
- if (empty($this->multiValues[$fieldKey])) {
- $this->multiValues[$fieldKey][] = '';
- }
- }
-
- // Livewire magic method to validate MultiValue input boxes
- // Runs on every debounce
- public function updatedMultiValues($value, $key)
- {
- $this->validate([
- 'multiValues.*.*' => ['nullable', 'string', 'regex:/^[^,]*$/'],
- ], [
- 'multiValues.*.*.regex' => 'Items cannot contain commas.',
- ]);
- }
-
- public function loadXhrSelectOptions(string $fieldKey, string $endpoint, ?string $query = null): void
- {
- abort_unless(auth()->user()->plugins->contains($this->plugin), 403);
-
- try {
- $requestData = [];
- if ($query !== null) {
- $requestData = [
- 'function' => $fieldKey,
- 'query' => $query
- ];
- }
-
- $response = $query !== null
- ? Http::post($endpoint, $requestData)
- : Http::post($endpoint);
-
- if ($response->successful()) {
- $this->xhrSelectOptions[$fieldKey] = $response->json();
- } else {
- $this->xhrSelectOptions[$fieldKey] = [];
- }
- } catch (\Exception $e) {
- $this->xhrSelectOptions[$fieldKey] = [];
- }
- }
-
- public function searchXhrSelect(string $fieldKey, string $endpoint): void
- {
- $query = $this->searchQueries[$fieldKey] ?? '';
- if (!empty($query)) {
- $this->loadXhrSelectOptions($fieldKey, $endpoint, $query);
- }
- }
-};?>
-
-
-
-
-
- Configuration
- Configure your plugin settings
-
-
-
-
-
-
diff --git a/resources/views/livewire/plugins/image-webhook-instance.blade.php b/resources/views/livewire/plugins/image-webhook-instance.blade.php
deleted file mode 100644
index e4ad9df..0000000
--- a/resources/views/livewire/plugins/image-webhook-instance.blade.php
+++ /dev/null
@@ -1,298 +0,0 @@
-user()->plugins->contains($this->plugin), 403);
- abort_unless($this->plugin->plugin_type === 'image_webhook', 404);
-
- $this->name = $this->plugin->name;
- }
-
- protected array $rules = [
- 'name' => 'required|string|max:255',
- 'checked_devices' => 'array',
- 'device_playlist_names' => 'array',
- 'device_playlists' => 'array',
- 'device_weekdays' => 'array',
- 'device_active_from' => 'array',
- 'device_active_until' => 'array',
- ];
-
- public function updateName(): void
- {
- abort_unless(auth()->user()->plugins->contains($this->plugin), 403);
- $this->validate(['name' => 'required|string|max:255']);
- $this->plugin->update(['name' => $this->name]);
- }
-
-
- public function addToPlaylist()
- {
- $this->validate([
- 'checked_devices' => 'required|array|min:1',
- ]);
-
- foreach ($this->checked_devices as $deviceId) {
- if (!isset($this->device_playlists[$deviceId]) || empty($this->device_playlists[$deviceId])) {
- $this->addError('device_playlists.' . $deviceId, 'Please select a playlist for each device.');
- return;
- }
-
- if ($this->device_playlists[$deviceId] === 'new') {
- if (!isset($this->device_playlist_names[$deviceId]) || empty($this->device_playlist_names[$deviceId])) {
- $this->addError('device_playlist_names.' . $deviceId, 'Playlist name is required when creating a new playlist.');
- return;
- }
- }
- }
-
- foreach ($this->checked_devices as $deviceId) {
- $playlist = null;
-
- if ($this->device_playlists[$deviceId] === 'new') {
- $playlist = \App\Models\Playlist::create([
- 'device_id' => $deviceId,
- 'name' => $this->device_playlist_names[$deviceId],
- 'weekdays' => !empty($this->device_weekdays[$deviceId] ?? null) ? $this->device_weekdays[$deviceId] : null,
- 'active_from' => $this->device_active_from[$deviceId] ?? null,
- 'active_until' => $this->device_active_until[$deviceId] ?? null,
- ]);
- } else {
- $playlist = \App\Models\Playlist::findOrFail($this->device_playlists[$deviceId]);
- }
-
- $maxOrder = $playlist->items()->max('order') ?? 0;
-
- // Image webhook plugins only support full layout
- $playlist->items()->create([
- 'plugin_id' => $this->plugin->id,
- 'order' => $maxOrder + 1,
- ]);
- }
-
- $this->reset([
- 'checked_devices',
- 'device_playlists',
- 'device_playlist_names',
- 'device_weekdays',
- 'device_active_from',
- 'device_active_until',
- ]);
- Flux::modal('add-to-playlist')->close();
- }
-
- public function getDevicePlaylists($deviceId)
- {
- return \App\Models\Playlist::where('device_id', $deviceId)->get();
- }
-
- public function hasAnyPlaylistSelected(): bool
- {
- foreach ($this->checked_devices as $deviceId) {
- if (isset($this->device_playlists[$deviceId]) && !empty($this->device_playlists[$deviceId])) {
- return true;
- }
- }
- return false;
- }
-
- public function deletePlugin(): void
- {
- abort_unless(auth()->user()->plugins->contains($this->plugin), 403);
- $this->plugin->delete();
- $this->redirect(route('plugins.image-webhook'));
- }
-
- public function getImagePath(): ?string
- {
- if (!$this->plugin->current_image) {
- return null;
- }
-
- $extensions = ['png', 'bmp'];
- foreach ($extensions as $ext) {
- $path = 'images/generated/'.$this->plugin->current_image.'.'.$ext;
- if (\Illuminate\Support\Facades\Storage::disk('public')->exists($path)) {
- return $path;
- }
- }
-
- return null;
- }
-};
-?>
-
-
-
-
-
Image Webhook – {{$plugin->name}}
-
-
-
- Add to Playlist
-
-
-
-
-
-
- Delete Instance
-
-
-
-
-
-
-
-
-
- Add to Playlist
-
-
-
-
-
-
-
-
-
Delete {{ $plugin->name }}?
-
This will also remove this instance from your playlists.
-
-
-
-
-
- Cancel
-
- Delete instance
-
-
-
-
-
-
-
-
- Webhook URL
-
- POST an image (PNG or BMP) to this URL to update the displayed image.
-
-
- Images must be posted in a format that can directly be read by the device. You need to take care of image format, dithering, and bit-depth. Check device logs if the image is not shown.
-
-
-
-
-
-
-
-
Current Image
- @if($this->getImagePath())
-
- @else
-
- No image uploaded yet. POST an image to the webhook URL to get started.
-
- @endif
-
-
-
-
-
-
diff --git a/resources/views/livewire/plugins/image-webhook.blade.php b/resources/views/livewire/plugins/image-webhook.blade.php
deleted file mode 100644
index 3161443..0000000
--- a/resources/views/livewire/plugins/image-webhook.blade.php
+++ /dev/null
@@ -1,163 +0,0 @@
- 'required|string|max:255',
- ];
-
- public function mount(): void
- {
- $this->refreshInstances();
- }
-
- public function refreshInstances(): void
- {
- $this->instances = auth()->user()
- ->plugins()
- ->where('plugin_type', 'image_webhook')
- ->orderBy('created_at', 'desc')
- ->get()
- ->toArray();
- }
-
- public function createInstance(): void
- {
- abort_unless(auth()->user() !== null, 403);
- $this->validate();
-
- Plugin::create([
- 'uuid' => Str::uuid(),
- 'user_id' => auth()->id(),
- 'name' => $this->name,
- 'plugin_type' => 'image_webhook',
- 'data_strategy' => 'static', // Not used for image_webhook, but required
- 'data_stale_minutes' => 60, // Not used for image_webhook, but required
- ]);
-
- $this->reset(['name']);
- $this->refreshInstances();
-
- Flux::modal('create-instance')->close();
- }
-
- public function deleteInstance(int $pluginId): void
- {
- abort_unless(auth()->user() !== null, 403);
-
- $plugin = Plugin::where('id', $pluginId)
- ->where('user_id', auth()->id())
- ->where('plugin_type', 'image_webhook')
- ->firstOrFail();
-
- $plugin->delete();
- $this->refreshInstances();
- }
-};
-?>
-
-
-
-
-
Image Webhook
- Plugin
-
-
- Create Instance
-
-
-
-
-
-
- Create Image Webhook Instance
- Create a new instance that accepts images via webhook
-
-
-
-
-
-
- @if(empty($instances))
-
-
- No instances yet
- Create your first Image Webhook instance to get started.
-
-
- @else
-
-
-
-
- Name
-
-
- Actions
-
-
-
-
-
- @foreach($instances as $instance)
-
-
- {{ $instance['name'] }}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- @endforeach
-
-
- @endif
-
- @foreach($instances as $instance)
-
-
-
Delete {{ $instance['name'] }}?
-
This will also remove this instance from your playlists.
-
-
-
-
-
- Cancel
-
- Delete instance
-
-
- @endforeach
-
-
-
diff --git a/resources/views/livewire/plugins/index.blade.php b/resources/views/livewire/plugins/index.blade.php
index d902183..469365c 100644
--- a/resources/views/livewire/plugins/index.blade.php
+++ b/resources/views/livewire/plugins/index.blade.php
@@ -26,8 +26,6 @@ new class extends Component {
['name' => 'Markup', 'flux_icon_name' => 'code-bracket', 'detail_view_route' => 'plugins.markup'],
'api' =>
['name' => 'API', 'flux_icon_name' => 'braces', 'detail_view_route' => 'plugins.api'],
- 'image-webhook' =>
- ['name' => 'Image Webhook', 'flux_icon_name' => 'photo', 'detail_view_route' => 'plugins.image-webhook'],
];
protected $rules = [
@@ -42,12 +40,7 @@ new class extends Component {
public function refreshPlugins(): void
{
- // Only show recipe plugins in the main list (image_webhook has its own management page)
- $userPlugins = auth()->user()?->plugins()
- ->where('plugin_type', 'recipe')
- ->get()
- ->makeHidden(['render_markup', 'data_payload'])
- ->toArray();
+ $userPlugins = auth()->user()?->plugins?->makeHidden(['render_markup', 'data_payload'])->toArray();
$allPlugins = array_merge($this->native_plugins, $userPlugins ?? []);
$allPlugins = array_values($allPlugins);
$allPlugins = $this->sortPlugins($allPlugins);
@@ -395,7 +388,7 @@ new class extends Component {
wire:key="plugin-{{ $plugin['id'] ?? $plugin['name'] ?? $index }}"
x-data="{ pluginName: {{ json_encode(strtolower($plugin['name'] ?? '')) }} }"
x-show="searchTerm.length <= 1 || pluginName.includes(searchTerm.toLowerCase())"
- class="styled-container">
+ class="rounded-xl border bg-white dark:bg-stone-950 dark:border-stone-800 text-stone-800 shadow-xs">
diff --git a/resources/views/livewire/plugins/recipe.blade.php b/resources/views/livewire/plugins/recipe.blade.php
index 0e29e76..4be96cc 100644
--- a/resources/views/livewire/plugins/recipe.blade.php
+++ b/resources/views/livewire/plugins/recipe.blade.php
@@ -1,16 +1,12 @@
user()->plugins->contains($this->plugin), 403);
$this->blade_code = $this->plugin->render_markup;
- // required to render some stuff
$this->configuration_template = $this->plugin->configuration_template ?? [];
+ $this->configuration = is_array($this->plugin->configuration) ? $this->plugin->configuration : [];
if ($this->plugin->render_markup_view) {
try {
@@ -77,12 +74,6 @@ new class extends Component {
$this->fillformFields();
$this->data_payload_updated_at = $this->plugin->data_payload_updated_at;
-
- // Set default preview device model
- if ($this->preview_device_model_id === null) {
- $defaultModel = DeviceModel::where('name', 'og_plus')->first() ?? DeviceModel::first();
- $this->preview_device_model_id = $defaultModel?->id;
- }
}
public function fillFormFields(): void
@@ -138,19 +129,6 @@ new class extends Component {
$validated = $this->validate();
$validated['data_payload'] = json_decode(Arr::get($validated,'data_payload'), true);
$this->plugin->update($validated);
-
- foreach ($this->configuration_template as $fieldKey => $field) {
- if (($field['field_type'] ?? null) !== 'multi_string') {
- continue;
- }
-
- if (!isset($this->multiValues[$fieldKey])) {
- continue;
- }
-
- $validated[$fieldKey] = implode(',', array_filter(array_map('trim', $this->multiValues[$fieldKey])));
- }
-
}
protected function validatePollingUrl(): void
@@ -276,6 +254,39 @@ new class extends Component {
Flux::modal('add-to-playlist')->close();
}
+ public function saveConfiguration()
+ {
+ abort_unless(auth()->user()->plugins->contains($this->plugin), 403);
+
+ $configurationValues = [];
+ if (isset($this->configuration_template['custom_fields'])) {
+ foreach ($this->configuration_template['custom_fields'] as $field) {
+ $fieldKey = $field['keyname'];
+ if (isset($this->configuration[$fieldKey])) {
+ $value = $this->configuration[$fieldKey];
+
+ // For code fields, if the value is a JSON string and the original was an array, decode it
+ if ($field['field_type'] === 'code' && is_string($value)) {
+ $decoded = json_decode($value, true);
+ // If it's valid JSON and decodes to an array/object, use the decoded value
+ // Otherwise, keep the string as-is
+ if (json_last_error() === JSON_ERROR_NONE && (is_array($decoded) || is_object($decoded))) {
+ $value = $decoded;
+ }
+ }
+
+ $configurationValues[$fieldKey] = $value;
+ }
+ }
+ }
+
+ $this->plugin->update([
+ 'configuration' => $configurationValues
+ ]);
+
+ Flux::modal('configuration-modal')->close();
+ }
+
public function getDevicePlaylists($deviceId)
{
return \App\Models\Playlist::where('device_id', $deviceId)->get();
@@ -296,6 +307,8 @@ new class extends Component {
return $this->configuration[$key] ?? $default;
}
+
+
public function renderExample(string $example)
{
switch ($example) {
@@ -364,17 +377,13 @@ HTML;
{
abort_unless(auth()->user()->plugins->contains($this->plugin), 403);
- $this->preview_size = $size;
-
// If data strategy is polling and data_payload is null, fetch the data first
if ($this->plugin->data_strategy === 'polling' && $this->plugin->data_payload === null) {
$this->updateData();
}
try {
- // Create a device object with og_plus model and the selected bitdepth
- $device = $this->createPreviewDevice();
- $previewMarkup = $this->plugin->render($size, true, $device);
+ $previewMarkup = $this->plugin->render($size);
$this->dispatch('preview-updated', preview: $previewMarkup);
} catch (LiquidException $e) {
$this->dispatch('preview-error', message: $e->toLiquidErrorMessage());
@@ -383,38 +392,6 @@ HTML;
}
}
- private function createPreviewDevice(): \App\Models\Device
- {
- $deviceModel = DeviceModel::with(['palette'])->find($this->preview_device_model_id)
- ?? DeviceModel::with(['palette'])->first();
-
- $device = new Device();
- $device->setRelation('deviceModel', $deviceModel);
-
- return $device;
- }
-
- public function getDeviceModels()
- {
- return DeviceModel::whereKind('trmnl')->orderBy('label')->get();
- }
-
- public function updatedPreviewDeviceModelId(): void
- {
- $this->renderPreview($this->preview_size);
- }
-
- public function duplicatePlugin(): void
- {
- abort_unless(auth()->user()->plugins->contains($this->plugin), 403);
-
- // Use the model's duplicate method
- $newPlugin = $this->plugin->duplicate(auth()->id());
-
- // Redirect to the new plugin's detail page
- $this->redirect(route('plugins.recipe', ['plugin' => $newPlugin]));
- }
-
public function deletePlugin(): void
{
abort_unless(auth()->user()->plugins->contains($this->plugin), 403);
@@ -422,31 +399,42 @@ HTML;
$this->redirect(route('plugins.index'));
}
- #[On('config-updated')]
- public function refreshPlugin()
- {
- // This pulls the fresh 'configuration' from the DB
- // and re-triggers the @if check in the Blade template
- $this->plugin = $this->plugin->fresh();
- }
+ public function loadXhrSelectOptions(string $fieldKey, string $endpoint, ?string $query = null): void
+ {
+ abort_unless(auth()->user()->plugins->contains($this->plugin), 403);
- // Laravel Livewire computed property: access with $this->parsed_urls
- #[Computed]
- private function parsedUrls()
- {
- if (!isset($this->polling_url)) {
- return null;
+ try {
+ $requestData = [];
+ if ($query !== null) {
+ $requestData = [
+ 'function' => $fieldKey,
+ 'query' => $query
+ ];
+ }
+
+ $response = $query !== null
+ ? Http::post($endpoint, $requestData)
+ : Http::post($endpoint);
+
+ if ($response->successful()) {
+ $this->xhrSelectOptions[$fieldKey] = $response->json();
+ } else {
+ $this->xhrSelectOptions[$fieldKey] = [];
+ }
+ } catch (\Exception $e) {
+ $this->xhrSelectOptions[$fieldKey] = [];
+ }
}
- try {
- return $this->plugin->resolveLiquidVariables($this->polling_url);
-
- } catch (\Exception $e) {
- return 'PARSE_ERROR: ' . $e->getMessage();
+ public function searchXhrSelect(string $fieldKey, string $endpoint): void
+ {
+ $query = $this->searchQueries[$fieldKey] ?? '';
+ if (!empty($query)) {
+ $this->loadXhrSelectOptions($fieldKey, $endpoint, $query);
+ }
}
- }
-
}
+
?>
@@ -478,6 +466,7 @@ HTML;
+
@@ -487,11 +476,6 @@ HTML;
-
- Recipe Settings
-
-
- Duplicate Plugin
Delete Plugin
@@ -633,15 +617,8 @@ HTML;
-
+
Preview {{ $plugin->name }}
-
-
- @foreach($this->getDeviceModels() as $model)
- {{ $model->label ?? $model->name }}
- @endforeach
-
-
@@ -649,9 +626,269 @@ HTML;
-
+
+
+
+ Configuration
+ Configure your plugin settings
+
-
+
+
+
Settings
@@ -739,7 +976,7 @@ HTML;
@endif
- Configuration Fields
+ Configuration
@endif
@@ -752,62 +989,15 @@ HTML;
@if($data_strategy === 'polling')
-
Polling URL
-
-
-
-
-
- Settings
-
-
-
-
- Preview URL
-
-
-
-
-
-
- Enter the URL(s) to poll for data:
-
+
-
- {!! 'Hint: Supports multiple requests via line break separation. You can also use configuration variables with Liquid syntax . ' !!}
-
-
-
-
-
-
- Preview computed URLs here (readonly):
-
- {{ $this->parsed_urls }}
-
-
-
-
-
+ class="block w-full" type="text" name="polling_url" autofocus>
+
+
Fetch data now
-
@@ -950,7 +1140,7 @@ HTML;
/>
diff --git a/resources/views/livewire/plugins/recipes/settings.blade.php b/resources/views/livewire/plugins/recipes/settings.blade.php
deleted file mode 100644
index 8ae3d6f..0000000
--- a/resources/views/livewire/plugins/recipes/settings.blade.php
+++ /dev/null
@@ -1,104 +0,0 @@
-resetErrorBag();
- // Reload data
- $this->plugin = $this->plugin->fresh();
- $this->trmnlp_id = $this->plugin->trmnlp_id;
- $this->uuid = $this->plugin->uuid;
- $this->alias = $this->plugin->alias ?? false;
- }
-
- public function saveTrmnlpId(): void
- {
- abort_unless(auth()->user()->plugins->contains($this->plugin), 403);
-
- $this->validate([
- 'trmnlp_id' => [
- 'nullable',
- 'string',
- 'max:255',
- Rule::unique('plugins', 'trmnlp_id')
- ->where('user_id', auth()->id())
- ->ignore($this->plugin->id),
- ],
- 'alias' => 'boolean',
- ]);
-
- $this->plugin->update([
- 'trmnlp_id' => empty($this->trmnlp_id) ? null : $this->trmnlp_id,
- 'alias' => $this->alias,
- ]);
-
- Flux::modal('trmnlp-settings')->close();
- }
-
- public function getAliasUrlProperty(): string
- {
- return url("/api/display/{$this->uuid}/alias");
- }
-};?>
-
-
-
-
- Recipe Settings
-
-
-
-
-
diff --git a/resources/views/recipes/holidays-ical.blade.php b/resources/views/recipes/holidays-ical.blade.php
index 454709d..f5f5403 100644
--- a/resources/views/recipes/holidays-ical.blade.php
+++ b/resources/views/recipes/holidays-ical.blade.php
@@ -2,46 +2,36 @@
@php
use Carbon\Carbon;
- $today = Carbon::today(config('app.timezone'));
-
$events = collect($data['ical'] ?? [])
->map(function (array $event): array {
+ $start = null;
+ $end = null;
+
try {
- $start = isset($event['DTSTART'])
- ? Carbon::parse($event['DTSTART'])->setTimezone(config('app.timezone'))
- : null;
+ $start = isset($event['DTSTART']) ? Carbon::parse($event['DTSTART'])->setTimezone(config('app.timezone')) : null;
} catch (Exception $e) {
$start = null;
}
try {
- $end = isset($event['DTEND'])
- ? Carbon::parse($event['DTEND'])->setTimezone(config('app.timezone'))
- : null;
+ $end = isset($event['DTEND']) ? Carbon::parse($event['DTEND'])->setTimezone(config('app.timezone')) : null;
} catch (Exception $e) {
$end = null;
}
return [
- 'summary' => $event['SUMMARY'] ?? 'Untitled event',
- 'location' => $event['LOCATION'] ?? '—',
- 'start' => $start,
- 'end' => $end,
+ 'summary' => $event['SUMMARY'] ?? 'Untitled event',
+ 'location' => $event['LOCATION'] ?? null,
+ 'start' => $start,
+ 'end' => $end,
];
})
- ->filter(fn ($event) =>
- $event['start'] &&
- (
- $event['start']->greaterThanOrEqualTo($today) ||
- ($event['end'] && $event['end']->greaterThanOrEqualTo($today))
- )
- )
+ ->filter(fn ($event) => $event['start'])
->sortBy('start')
->take($size === 'quadrant' ? 5 : 8)
->values();
@endphp
-
diff --git a/resources/views/recipes/zen.blade.php b/resources/views/recipes/zen.blade.php
index 0ae920f..5e01eac 100644
--- a/resources/views/recipes/zen.blade.php
+++ b/resources/views/recipes/zen.blade.php
@@ -3,11 +3,11 @@
- {{$data['data'][0]['a'] ?? ''}}
- @if (strlen($data['data'][0]['q'] ?? '') < 300 && $size != 'quadrant')
- {{ $data['data'][0]['q'] ?? '' }}
+ {{$data[0]['a']}}
+ @if (strlen($data[0]['q']) < 300 && $size != 'quadrant')
+ {{ $data[0]['q'] }}
@else
- {{ $data['data'][0]['q'] ?? '' }}
+ {{ $data[0]['q'] }}
@endif
diff --git a/routes/api.php b/routes/api.php
index d201312..9721a0f 100644
--- a/routes/api.php
+++ b/routes/api.php
@@ -18,16 +18,18 @@ use Illuminate\Support\Str;
Route::get('/display', function (Request $request) {
$mac_address = $request->header('id');
$access_token = $request->header('access-token');
- $device = Device::where('api_key', $access_token)->first();
+ $device = Device::where('mac_address', $mac_address)
+ ->where('api_key', $access_token)
+ ->first();
if (! $device) {
// Check if there's a user with assign_new_devices enabled
$auto_assign_user = User::where('assign_new_devices', true)->first();
- if ($auto_assign_user && $mac_address) {
+ if ($auto_assign_user) {
// Create a new device and assign it to this user
$device = Device::create([
- 'mac_address' => mb_strtoupper($mac_address ?? ''),
+ 'mac_address' => $mac_address,
'api_key' => $access_token,
'user_id' => $auto_assign_user->id,
'name' => "{$auto_assign_user->name}'s TRMNL",
@@ -37,7 +39,7 @@ Route::get('/display', function (Request $request) {
]);
} else {
return response()->json([
- 'message' => 'MAC Address not registered (or not set), or invalid access token',
+ 'message' => 'MAC Address not registered or invalid access token',
], 404);
}
}
@@ -93,16 +95,9 @@ Route::get('/display', function (Request $request) {
// Check and update stale data if needed
if ($plugin->isDataStale() || $plugin->current_image === null) {
$plugin->updateDataPayload();
- try {
- $markup = $plugin->render(device: $device);
+ $markup = $plugin->render(device: $device);
- GenerateScreenJob::dispatchSync($device->id, $plugin->id, $markup);
- } catch (Exception $e) {
- Log::error("Failed to render plugin {$plugin->id} ({$plugin->name}): ".$e->getMessage());
- // Generate error display
- $errorImageUuid = ImageGenerationService::generateDefaultScreenImage($device, 'error', $plugin->name);
- $device->update(['current_screen_image' => $errorImageUuid]);
- }
+ GenerateScreenJob::dispatchSync($device->id, $plugin->id, $markup);
}
$plugin->refresh();
@@ -125,17 +120,8 @@ Route::get('/display', function (Request $request) {
}
}
- try {
- $markup = $playlistItem->render(device: $device);
- GenerateScreenJob::dispatchSync($device->id, null, $markup);
- } catch (Exception $e) {
- Log::error("Failed to render mashup playlist item {$playlistItem->id}: ".$e->getMessage());
- // For mashups, show error for the first plugin or a generic error
- $firstPlugin = $plugins->first();
- $pluginName = $firstPlugin ? $firstPlugin->name : 'Recipe';
- $errorImageUuid = ImageGenerationService::generateDefaultScreenImage($device, 'error', $pluginName);
- $device->update(['current_screen_image' => $errorImageUuid]);
- }
+ $markup = $playlistItem->render(device: $device);
+ GenerateScreenJob::dispatchSync($device->id, null, $markup);
$device->refresh();
@@ -218,7 +204,7 @@ Route::get('/setup', function (Request $request) {
], 404);
}
- $device = Device::where('mac_address', mb_strtoupper($mac_address))->first();
+ $device = Device::where('mac_address', $mac_address)->first();
if (! $device) {
// Check if there's a user with assign_new_devices enabled
@@ -233,7 +219,7 @@ Route::get('/setup', function (Request $request) {
// Create a new device and assign it to this user
$device = Device::create([
- 'mac_address' => mb_strtoupper($mac_address),
+ 'mac_address' => $mac_address,
'api_key' => Str::random(22),
'user_id' => $auto_assign_user->id,
'name' => "{$auto_assign_user->name}'s TRMNL",
@@ -359,7 +345,7 @@ Route::post('/display/update', function (Request $request) {
Route::post('/screens', function (Request $request) {
$mac_address = $request->header('id');
$access_token = $request->header('access-token');
- $device = Device::where('mac_address', mb_strtoupper($mac_address ?? ''))
+ $device = Device::where('mac_address', $mac_address)
->where('api_key', $access_token)
->first();
@@ -547,91 +533,6 @@ Route::post('custom_plugins/{plugin_uuid}', function (string $plugin_uuid) {
return response()->json(['message' => 'Data updated successfully']);
})->name('api.custom_plugins.webhook');
-Route::post('plugin_settings/{uuid}/image', function (Request $request, string $uuid) {
- $plugin = Plugin::where('uuid', $uuid)->firstOrFail();
-
- // Check if plugin is image_webhook type
- if ($plugin->plugin_type !== 'image_webhook') {
- return response()->json(['error' => 'Plugin is not an image webhook plugin'], 400);
- }
-
- // Accept image from either multipart form or raw binary
- $image = null;
- $extension = null;
-
- if ($request->hasFile('image')) {
- $file = $request->file('image');
- $extension = mb_strtolower($file->getClientOriginalExtension());
- $image = $file->get();
- } elseif ($request->has('image')) {
- // Base64 encoded image
- $imageData = $request->input('image');
- if (preg_match('/^data:image\/(\w+);base64,/', $imageData, $matches)) {
- $extension = mb_strtolower($matches[1]);
- $image = base64_decode(mb_substr($imageData, mb_strpos($imageData, ',') + 1));
- } else {
- return response()->json(['error' => 'Invalid image format. Expected base64 data URI.'], 400);
- }
- } else {
- // Try raw binary
- $image = $request->getContent();
- $contentType = $request->header('Content-Type', '');
- $trimmedContent = mb_trim($image);
-
- // Check if content is empty or just empty JSON
- if (empty($image) || $trimmedContent === '' || $trimmedContent === '{}') {
- return response()->json(['error' => 'No image data provided'], 400);
- }
-
- // If it's a JSON request without image field, return error
- if (str_contains($contentType, 'application/json')) {
- return response()->json(['error' => 'No image data provided'], 400);
- }
-
- // Detect image type from content
- $finfo = finfo_open(FILEINFO_MIME_TYPE);
- $mimeType = finfo_buffer($finfo, $image);
- finfo_close($finfo);
-
- $extension = match ($mimeType) {
- 'image/png' => 'png',
- 'image/bmp' => 'bmp',
- default => null,
- };
-
- if (! $extension) {
- return response()->json(['error' => 'Unsupported image format. Expected PNG or BMP.'], 400);
- }
- }
-
- // Validate extension
- $allowedExtensions = ['png', 'bmp'];
- if (! in_array($extension, $allowedExtensions)) {
- return response()->json(['error' => 'Unsupported image format. Expected PNG or BMP.'], 400);
- }
-
- // Generate a new UUID for each image upload to prevent device caching
- $imageUuid = Str::uuid()->toString();
- $filename = $imageUuid.'.'.$extension;
- $path = 'images/generated/'.$filename;
-
- // Save image to storage
- Storage::disk('public')->put($path, $image);
-
- // Update plugin's current_image field with the new UUID
- $plugin->update([
- 'current_image' => $imageUuid,
- ]);
-
- // Clean up old images
- ImageGenerationService::cleanupFolder();
-
- return response()->json([
- 'message' => 'Image uploaded successfully',
- 'image_url' => url('storage/'.$path),
- ]);
-})->name('api.plugin_settings.image');
-
Route::get('plugin_settings/{trmnlp_id}/archive', function (Request $request, string $trmnlp_id) {
if (! $trmnlp_id || mb_trim($trmnlp_id) === '') {
return response()->json([
@@ -676,90 +577,3 @@ Route::post('plugin_settings/{trmnlp_id}/archive', function (Request $request, s
],
]);
})->middleware('auth:sanctum');
-
-Route::get('/display/{uuid}/alias', function (Request $request, string $uuid) {
- $plugin = Plugin::where('uuid', $uuid)->firstOrFail();
-
- // Check if alias is active
- if (! $plugin->alias) {
- return response()->json([
- 'message' => 'Alias is not active for this plugin',
- ], 403);
- }
-
- // Get device model name from query parameter, default to 'og_png'
- $deviceModelName = $request->query('device-model', 'og_png');
- $deviceModel = DeviceModel::where('name', $deviceModelName)->first();
-
- if (! $deviceModel) {
- return response()->json([
- 'message' => "Device model '{$deviceModelName}' not found",
- ], 404);
- }
-
- // Check if we can use cached image (only for og_png and if data is not stale)
- $useCache = $deviceModelName === 'og_png' && ! $plugin->isDataStale() && $plugin->current_image !== null;
-
- if ($useCache) {
- // Return cached image
- $imageUuid = $plugin->current_image;
- $fileExtension = $deviceModel->mime_type === 'image/bmp' ? 'bmp' : 'png';
- $imagePath = 'images/generated/'.$imageUuid.'.'.$fileExtension;
-
- // Check if image exists, otherwise fall back to generation
- if (Storage::disk('public')->exists($imagePath)) {
- return response()->file(Storage::disk('public')->path($imagePath), [
- 'Content-Type' => $deviceModel->mime_type,
- ]);
- }
- }
-
- // Generate new image
- try {
- // Update data if needed
- if ($plugin->isDataStale()) {
- $plugin->updateDataPayload();
- $plugin->refresh();
- }
-
- // Load device model with palette relationship
- $deviceModel->load('palette');
-
- // Create a virtual device for rendering (Plugin::render needs a Device object)
- $virtualDevice = new Device();
- $virtualDevice->setRelation('deviceModel', $deviceModel);
- $virtualDevice->setRelation('user', $plugin->user);
- $virtualDevice->setRelation('palette', $deviceModel->palette);
-
- // Render the plugin markup
- $markup = $plugin->render(device: $virtualDevice);
-
- // Generate image using the new method that doesn't require a device
- $imageUuid = ImageGenerationService::generateImageFromModel(
- markup: $markup,
- deviceModel: $deviceModel,
- user: $plugin->user,
- palette: $deviceModel->palette
- );
-
- // Update plugin cache if using og_png
- if ($deviceModelName === 'og_png') {
- $plugin->update(['current_image' => $imageUuid]);
- }
-
- // Return the generated image
- $fileExtension = $deviceModel->mime_type === 'image/bmp' ? 'bmp' : 'png';
- $imagePath = Storage::disk('public')->path('images/generated/'.$imageUuid.'.'.$fileExtension);
-
- return response()->file($imagePath, [
- 'Content-Type' => $deviceModel->mime_type,
- ]);
- } catch (Exception $e) {
- Log::error("Failed to generate alias image for plugin {$plugin->id} ({$plugin->name}): ".$e->getMessage());
-
- return response()->json([
- 'message' => 'Failed to generate image',
- 'error' => $e->getMessage(),
- ], 500);
- }
-})->name('api.display.alias');
diff --git a/routes/web.php b/routes/web.php
index b3069bd..7b7868d 100644
--- a/routes/web.php
+++ b/routes/web.php
@@ -31,8 +31,6 @@ Route::middleware(['auth'])->group(function () {
Volt::route('plugins/recipe/{plugin}', 'plugins.recipe')->name('plugins.recipe');
Volt::route('plugins/markup', 'plugins.markup')->name('plugins.markup');
Volt::route('plugins/api', 'plugins.api')->name('plugins.api');
- Volt::route('plugins/image-webhook', 'plugins.image-webhook')->name('plugins.image-webhook');
- Volt::route('plugins/image-webhook/{plugin}', 'plugins.image-webhook-instance')->name('plugins.image-webhook-instance');
Volt::route('playlists', 'playlists.index')->name('playlists.index');
Route::get('plugin_settings/{trmnlp_id}/edit', function (Request $request, string $trmnlp_id) {
diff --git a/tests/Feature/Api/DeviceEndpointsTest.php b/tests/Feature/Api/DeviceEndpointsTest.php
index c98cb2f..726f313 100644
--- a/tests/Feature/Api/DeviceEndpointsTest.php
+++ b/tests/Feature/Api/DeviceEndpointsTest.php
@@ -7,7 +7,6 @@ use App\Models\Playlist;
use App\Models\PlaylistItem;
use App\Models\Plugin;
use App\Models\User;
-use App\Services\ImageGenerationService;
use Bnussbau\TrmnlPipeline\TrmnlPipeline;
use Illuminate\Support\Facades\Queue;
use Illuminate\Support\Facades\Storage;
@@ -263,7 +262,7 @@ test('invalid device credentials return error', function (): void {
])->get('/api/display');
$response->assertNotFound()
- ->assertJson(['message' => 'MAC Address not registered (or not set), or invalid access token']);
+ ->assertJson(['message' => 'MAC Address not registered or invalid access token']);
});
test('log endpoint requires valid device credentials', function (): void {
@@ -955,232 +954,3 @@ test('setup endpoint handles non-existent device model gracefully', function ():
expect($device)->not->toBeNull()
->and($device->device_model_id)->toBeNull();
});
-
-test('setup endpoint matches MAC address case-insensitively', function (): void {
- // Create device with lowercase MAC address
- $device = Device::factory()->create([
- 'mac_address' => 'a1:b2:c3:d4:e5:f6',
- 'api_key' => 'test-api-key',
- 'friendly_id' => 'test-device',
- ]);
-
- // Request with uppercase MAC address should still match
- $response = $this->withHeaders([
- 'id' => 'A1:B2:C3:D4:E5:F6',
- ])->get('/api/setup');
-
- $response->assertOk()
- ->assertJson([
- 'status' => 200,
- 'api_key' => 'test-api-key',
- 'friendly_id' => 'test-device',
- 'message' => 'Welcome to TRMNL BYOS',
- ]);
-});
-
-test('display endpoint matches MAC address case-insensitively', function (): void {
- // Create device with lowercase MAC address
- $device = Device::factory()->create([
- 'mac_address' => 'a1:b2:c3:d4:e5:f6',
- 'api_key' => 'test-api-key',
- 'current_screen_image' => 'test-image',
- ]);
-
- // Request with uppercase MAC address should still match
- $response = $this->withHeaders([
- 'id' => 'A1:B2:C3:D4:E5:F6',
- 'access-token' => $device->api_key,
- 'rssi' => -70,
- 'battery_voltage' => 3.8,
- 'fw-version' => '1.0.0',
- ])->get('/api/display');
-
- $response->assertOk()
- ->assertJson([
- 'status' => '0',
- 'filename' => 'test-image.bmp',
- ]);
-});
-
-test('screens endpoint matches MAC address case-insensitively', function (): void {
- Queue::fake();
-
- // Create device with uppercase MAC address
- $device = Device::factory()->create([
- 'mac_address' => 'A1:B2:C3:D4:E5:F6',
- 'api_key' => 'test-api-key',
- ]);
-
- // Request with lowercase MAC address should still match
- $response = $this->withHeaders([
- 'id' => 'a1:b2:c3:d4:e5:f6',
- 'access-token' => $device->api_key,
- ])->post('/api/screens', [
- 'image' => [
- 'content' => 'Test content
',
- ],
- ]);
-
- $response->assertOk();
- Queue::assertPushed(GenerateScreenJob::class);
-});
-
-test('display endpoint handles plugin rendering errors gracefully', function (): void {
- TrmnlPipeline::fake();
-
- $device = Device::factory()->create([
- 'mac_address' => '00:11:22:33:44:55',
- 'api_key' => 'test-api-key',
- 'proxy_cloud' => false,
- ]);
-
- // Create a plugin with Blade markup that will cause an exception when accessing data[0]
- // when data is not an array or doesn't have index 0
- $plugin = Plugin::factory()->create([
- 'name' => 'Broken Recipe',
- 'data_strategy' => 'polling',
- 'polling_url' => null,
- 'data_stale_minutes' => 1,
- 'markup_language' => 'blade', // Use Blade which will throw exception on invalid array access
- 'render_markup' => '{{ $data[0]["invalid"] }}
', // This will fail if data[0] doesn't exist
- 'data_payload' => ['error' => 'Failed to fetch data'], // Not a list, so data[0] will fail
- 'data_payload_updated_at' => now()->subMinutes(2), // Make it stale
- 'current_image' => null,
- ]);
-
- $playlist = Playlist::factory()->create([
- 'device_id' => $device->id,
- 'name' => 'test_playlist',
- 'is_active' => true,
- 'weekdays' => null,
- 'active_from' => null,
- 'active_until' => null,
- ]);
-
- PlaylistItem::factory()->create([
- 'playlist_id' => $playlist->id,
- 'plugin_id' => $plugin->id,
- 'order' => 1,
- 'is_active' => true,
- 'last_displayed_at' => null,
- ]);
-
- $response = $this->withHeaders([
- 'id' => $device->mac_address,
- 'access-token' => $device->api_key,
- 'rssi' => -70,
- 'battery_voltage' => 3.8,
- 'fw-version' => '1.0.0',
- ])->get('/api/display');
-
- $response->assertOk();
-
- // Verify error screen was generated and set on device
- $device->refresh();
- expect($device->current_screen_image)->not->toBeNull();
-
- // Verify the error image exists
- $errorImagePath = Storage::disk('public')->path("images/generated/{$device->current_screen_image}.png");
- // The TrmnlPipeline is faked, so we just verify the UUID was set
- expect($device->current_screen_image)->toBeString();
-});
-
-test('display endpoint handles mashup rendering errors gracefully', function (): void {
- TrmnlPipeline::fake();
-
- $device = Device::factory()->create([
- 'mac_address' => '00:11:22:33:44:55',
- 'api_key' => 'test-api-key',
- 'proxy_cloud' => false,
- ]);
-
- // Create plugins for mashup, one with invalid markup
- $plugin1 = Plugin::factory()->create([
- 'name' => 'Working Plugin',
- 'data_strategy' => 'polling',
- 'polling_url' => null,
- 'data_stale_minutes' => 1,
- 'render_markup_view' => 'trmnl',
- 'data_payload_updated_at' => now()->subMinutes(2),
- 'current_image' => null,
- ]);
-
- $plugin2 = Plugin::factory()->create([
- 'name' => 'Broken Plugin',
- 'data_strategy' => 'polling',
- 'polling_url' => null,
- 'data_stale_minutes' => 1,
- 'markup_language' => 'blade', // Use Blade which will throw exception on invalid array access
- 'render_markup' => '{{ $data[0]["invalid"] }}
', // This will fail
- 'data_payload' => ['error' => 'Failed to fetch data'],
- 'data_payload_updated_at' => now()->subMinutes(2),
- 'current_image' => null,
- ]);
-
- $playlist = Playlist::factory()->create([
- 'device_id' => $device->id,
- 'name' => 'test_playlist',
- 'is_active' => true,
- 'weekdays' => null,
- 'active_from' => null,
- 'active_until' => null,
- ]);
-
- // Create mashup playlist item
- $playlistItem = PlaylistItem::createMashup(
- $playlist,
- '1Lx1R',
- [$plugin1->id, $plugin2->id],
- 'Test Mashup',
- 1
- );
-
- $response = $this->withHeaders([
- 'id' => $device->mac_address,
- 'access-token' => $device->api_key,
- 'rssi' => -70,
- 'battery_voltage' => 3.8,
- 'fw-version' => '1.0.0',
- ])->get('/api/display');
-
- $response->assertOk();
-
- // Verify error screen was generated and set on device
- $device->refresh();
- expect($device->current_screen_image)->not->toBeNull();
-
- // Verify the error image UUID was set
- expect($device->current_screen_image)->toBeString();
-});
-
-test('generateDefaultScreenImage creates error screen with plugin name', function (): void {
- TrmnlPipeline::fake();
- Storage::fake('public');
- Storage::disk('public')->makeDirectory('/images/generated');
-
- $device = Device::factory()->create();
-
- $errorUuid = ImageGenerationService::generateDefaultScreenImage($device, 'error', 'Test Recipe Name');
-
- expect($errorUuid)->not->toBeEmpty();
-
- // Verify the error image path would be created
- $errorPath = "images/generated/{$errorUuid}.png";
- // Since TrmnlPipeline is faked, we just verify the UUID was generated
- expect($errorUuid)->toBeString();
-});
-
-test('generateDefaultScreenImage throws exception for invalid error image type', function (): void {
- $device = Device::factory()->create();
-
- expect(fn (): string => ImageGenerationService::generateDefaultScreenImage($device, 'invalid-error-type'))
- ->toThrow(InvalidArgumentException::class);
-});
-
-test('getDeviceSpecificDefaultImage returns null for error type when no device-specific image exists', function (): void {
- $device = new Device();
- $device->deviceModel = null;
-
- $result = ImageGenerationService::getDeviceSpecificDefaultImage($device, 'error');
- expect($result)->toBeNull();
-});
diff --git a/tests/Feature/Api/ImageWebhookTest.php b/tests/Feature/Api/ImageWebhookTest.php
deleted file mode 100644
index 121f90a..0000000
--- a/tests/Feature/Api/ImageWebhookTest.php
+++ /dev/null
@@ -1,196 +0,0 @@
-makeDirectory('/images/generated');
-});
-
-test('can upload image to image webhook plugin via multipart form', function (): void {
- $user = User::factory()->create();
- $plugin = Plugin::factory()->imageWebhook()->create([
- 'user_id' => $user->id,
- ]);
-
- $image = UploadedFile::fake()->image('test.png', 800, 480);
-
- $response = $this->post("/api/plugin_settings/{$plugin->uuid}/image", [
- 'image' => $image,
- ]);
-
- $response->assertOk()
- ->assertJsonStructure([
- 'message',
- 'image_url',
- ]);
-
- $plugin->refresh();
- expect($plugin->current_image)
- ->not->toBeNull()
- ->not->toBe($plugin->uuid); // Should be a new UUID, not the plugin's UUID
-
- // File should exist with the new UUID
- Storage::disk('public')->assertExists("images/generated/{$plugin->current_image}.png");
-
- // Image URL should contain the new UUID
- expect($response->json('image_url'))
- ->toContain($plugin->current_image);
-});
-
-test('can upload image to image webhook plugin via raw binary', function (): void {
- $user = User::factory()->create();
- $plugin = Plugin::factory()->imageWebhook()->create([
- 'user_id' => $user->id,
- ]);
-
- // Create a simple PNG image binary
- $pngData = base64_decode('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==');
-
- $response = $this->call('POST', "/api/plugin_settings/{$plugin->uuid}/image", [], [], [], [
- 'CONTENT_TYPE' => 'image/png',
- ], $pngData);
-
- $response->assertOk()
- ->assertJsonStructure([
- 'message',
- 'image_url',
- ]);
-
- $plugin->refresh();
- expect($plugin->current_image)
- ->not->toBeNull()
- ->not->toBe($plugin->uuid); // Should be a new UUID, not the plugin's UUID
-
- // File should exist with the new UUID
- Storage::disk('public')->assertExists("images/generated/{$plugin->current_image}.png");
-
- // Image URL should contain the new UUID
- expect($response->json('image_url'))
- ->toContain($plugin->current_image);
-});
-
-test('can upload image to image webhook plugin via base64 data URI', function (): void {
- $user = User::factory()->create();
- $plugin = Plugin::factory()->imageWebhook()->create([
- 'user_id' => $user->id,
- ]);
-
- // Create a simple PNG image as base64 data URI
- $base64Image = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==';
-
- $response = $this->postJson("/api/plugin_settings/{$plugin->uuid}/image", [
- 'image' => $base64Image,
- ]);
-
- $response->assertOk()
- ->assertJsonStructure([
- 'message',
- 'image_url',
- ]);
-
- $plugin->refresh();
- expect($plugin->current_image)
- ->not->toBeNull()
- ->not->toBe($plugin->uuid); // Should be a new UUID, not the plugin's UUID
-
- // File should exist with the new UUID
- Storage::disk('public')->assertExists("images/generated/{$plugin->current_image}.png");
-
- // Image URL should contain the new UUID
- expect($response->json('image_url'))
- ->toContain($plugin->current_image);
-});
-
-test('returns 400 for non-image-webhook plugin', function (): void {
- $user = User::factory()->create();
- $plugin = Plugin::factory()->create([
- 'user_id' => $user->id,
- 'plugin_type' => 'recipe',
- ]);
-
- $image = UploadedFile::fake()->image('test.png', 800, 480);
-
- $response = $this->post("/api/plugin_settings/{$plugin->uuid}/image", [
- 'image' => $image,
- ]);
-
- $response->assertStatus(400)
- ->assertJson(['error' => 'Plugin is not an image webhook plugin']);
-});
-
-test('returns 404 for non-existent plugin', function (): void {
- $image = UploadedFile::fake()->image('test.png', 800, 480);
-
- $response = $this->post('/api/plugin_settings/'.Str::uuid().'/image', [
- 'image' => $image,
- ]);
-
- $response->assertNotFound();
-});
-
-test('returns 400 for unsupported image format', function (): void {
- $user = User::factory()->create();
- $plugin = Plugin::factory()->imageWebhook()->create([
- 'user_id' => $user->id,
- ]);
-
- // Create a fake GIF file (not supported)
- $gifData = base64_decode('R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7');
-
- $response = $this->call('POST', "/api/plugin_settings/{$plugin->uuid}/image", [], [], [], [
- 'CONTENT_TYPE' => 'image/gif',
- ], $gifData);
-
- $response->assertStatus(400)
- ->assertJson(['error' => 'Unsupported image format. Expected PNG or BMP.']);
-});
-
-test('returns 400 for JPG image format', function (): void {
- $user = User::factory()->create();
- $plugin = Plugin::factory()->imageWebhook()->create([
- 'user_id' => $user->id,
- ]);
-
- // Create a fake JPG file (not supported)
- $jpgData = base64_decode('/9j/4AAQSkZJRgABAQEAYABgAAD/2wBDAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQH/2wBDAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQH/wAARCAABAAEDASIAAhEBAxEB/8QAFQABAQAAAAAAAAAAAAAAAAAAAAv/xAAUEAEAAAAAAAAAAAAAAAAAAAAA/8QAFQEBAQAAAAAAAAAAAAAAAAAAAAX/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwA/8A8A');
-
- $response = $this->call('POST', "/api/plugin_settings/{$plugin->uuid}/image", [], [], [], [
- 'CONTENT_TYPE' => 'image/jpeg',
- ], $jpgData);
-
- $response->assertStatus(400)
- ->assertJson(['error' => 'Unsupported image format. Expected PNG or BMP.']);
-});
-
-test('returns 400 when no image data provided', function (): void {
- $user = User::factory()->create();
- $plugin = Plugin::factory()->imageWebhook()->create([
- 'user_id' => $user->id,
- ]);
-
- $response = $this->postJson("/api/plugin_settings/{$plugin->uuid}/image", []);
-
- $response->assertStatus(400)
- ->assertJson(['error' => 'No image data provided']);
-});
-
-test('image webhook plugin isDataStale returns false', function (): void {
- $plugin = Plugin::factory()->imageWebhook()->create();
-
- expect($plugin->isDataStale())->toBeFalse();
-});
-
-test('image webhook plugin factory creates correct plugin type', function (): void {
- $plugin = Plugin::factory()->imageWebhook()->create();
-
- expect($plugin)
- ->plugin_type->toBe('image_webhook')
- ->data_strategy->toBe('static');
-});
diff --git a/tests/Feature/ImageGenerationServiceTest.php b/tests/Feature/ImageGenerationServiceTest.php
index 07bb6a6..603205e 100644
--- a/tests/Feature/ImageGenerationServiceTest.php
+++ b/tests/Feature/ImageGenerationServiceTest.php
@@ -324,30 +324,6 @@ it('resetIfNotCacheable preserves image for standard devices', function (): void
expect($plugin->current_image)->toBe('test-uuid');
});
-it('cache is reset when plugin markup changes', function (): void {
- // Create a plugin with cached image
- $plugin = App\Models\Plugin::factory()->create([
- 'current_image' => 'cached-uuid',
- 'render_markup' => 'Original markup
',
- ]);
-
- // Create devices with standard dimensions (cacheable)
- Device::factory()->count(2)->create([
- 'width' => 800,
- 'height' => 480,
- 'rotate' => 0,
- ]);
-
- // Update the plugin markup
- $plugin->update([
- 'render_markup' => 'Updated markup
',
- ]);
-
- // Assert cache was reset when markup changed
- $plugin->refresh();
- expect($plugin->current_image)->toBeNull();
-});
-
it('determines correct image format from device model', function (): void {
// Test BMP format detection
$bmpModel = DeviceModel::factory()->create([
diff --git a/tests/Feature/Jobs/FetchDeviceModelsJobTest.php b/tests/Feature/Jobs/FetchDeviceModelsJobTest.php
index f0be135..7674d7f 100644
--- a/tests/Feature/Jobs/FetchDeviceModelsJobTest.php
+++ b/tests/Feature/Jobs/FetchDeviceModelsJobTest.php
@@ -44,7 +44,6 @@ test('fetch device models job handles successful api response', function (): voi
'mime_type' => 'image/png',
'offset_x' => 0,
'offset_y' => 0,
- 'kind' => 'trmnl',
'published_at' => '2023-01-01T00:00:00Z',
],
],
@@ -75,7 +74,6 @@ test('fetch device models job handles successful api response', function (): voi
expect($deviceModel->mime_type)->toBe('image/png');
expect($deviceModel->offset_x)->toBe(0);
expect($deviceModel->offset_y)->toBe(0);
- // expect($deviceModel->kind)->toBe('trmnl');
expect($deviceModel->source)->toBe('api');
});
@@ -314,7 +312,6 @@ test('fetch device models job handles device model with partial data', function
expect($deviceModel->mime_type)->toBe('');
expect($deviceModel->offset_x)->toBe(0);
expect($deviceModel->offset_y)->toBe(0);
- expect($deviceModel->kind)->toBeNull();
expect($deviceModel->source)->toBe('api');
});
diff --git a/tests/Feature/Livewire/Catalog/IndexTest.php b/tests/Feature/Livewire/Catalog/IndexTest.php
index 1b2efba..22ab4b6 100644
--- a/tests/Feature/Livewire/Catalog/IndexTest.php
+++ b/tests/Feature/Livewire/Catalog/IndexTest.php
@@ -65,46 +65,6 @@ it('loads plugins from catalog URL', function (): void {
$component->assertSee('testuser');
$component->assertSee('A test plugin');
$component->assertSee('MIT');
- $component->assertSee('Preview');
-});
-
-it('hides preview button when screenshot_url is missing', function (): void {
- // Clear cache first to ensure fresh data
- Cache::forget('catalog_plugins');
-
- // Mock the HTTP response for the catalog URL without screenshot_url
- $catalogData = [
- 'test-plugin' => [
- 'name' => 'Test Plugin Without Screenshot',
- 'author' => ['name' => 'Test Author', 'github' => 'testuser'],
- 'author_bio' => [
- 'description' => 'A test plugin',
- ],
- 'license' => 'MIT',
- 'trmnlp' => [
- 'zip_url' => 'https://example.com/plugin.zip',
- ],
- 'byos' => [
- 'byos_laravel' => [
- 'compatibility' => true,
- ],
- ],
- 'logo_url' => 'https://example.com/logo.png',
- 'screenshot_url' => null,
- ],
- ];
-
- $yamlContent = Yaml::dump($catalogData);
-
- Http::fake([
- config('app.catalog_url') => Http::response($yamlContent, 200),
- ]);
-
- Livewire::withoutLazyLoading();
-
- Volt::test('catalog.index')
- ->assertSee('Test Plugin Without Screenshot')
- ->assertDontSeeHtml('variant="subtle" icon="eye"');
});
it('shows error when plugin not found', function (): void {
@@ -154,46 +114,3 @@ it('shows error when zip_url is missing', function (): void {
$component->assertHasErrors();
});
-
-it('can preview a plugin', function (): void {
- // Clear cache first to ensure fresh data
- Cache::forget('catalog_plugins');
-
- // Mock the HTTP response for the catalog URL
- $catalogData = [
- 'test-plugin' => [
- 'name' => 'Test Plugin',
- 'author' => ['name' => 'Test Author', 'github' => 'testuser'],
- 'author_bio' => [
- 'description' => 'A test plugin description',
- ],
- 'license' => 'MIT',
- 'trmnlp' => [
- 'zip_url' => 'https://example.com/plugin.zip',
- ],
- 'byos' => [
- 'byos_laravel' => [
- 'compatibility' => true,
- ],
- ],
- 'logo_url' => 'https://example.com/logo.png',
- 'screenshot_url' => 'https://example.com/screenshot.png',
- ],
- ];
-
- $yamlContent = Yaml::dump($catalogData);
-
- Http::fake([
- config('app.catalog_url') => Http::response($yamlContent, 200),
- ]);
-
- Livewire::withoutLazyLoading();
-
- Volt::test('catalog.index')
- ->assertSee('Test Plugin')
- ->call('previewPlugin', 'test-plugin')
- ->assertSet('previewingPlugin', 'test-plugin')
- ->assertSet('previewData.name', 'Test Plugin')
- ->assertSee('Preview Test Plugin')
- ->assertSee('A test plugin description');
-});
diff --git a/tests/Feature/Livewire/Plugins/ConfigModalTest.php b/tests/Feature/Livewire/Plugins/ConfigModalTest.php
deleted file mode 100644
index 4372991..0000000
--- a/tests/Feature/Livewire/Plugins/ConfigModalTest.php
+++ /dev/null
@@ -1,124 +0,0 @@
-create();
- $this->actingAs($user);
-
- $plugin = Plugin::create([
- 'uuid' => Str::uuid(),
- 'user_id' => $user->id,
- 'name' => 'Test Plugin',
- 'data_strategy' => 'static',
- 'configuration_template' => [
- 'custom_fields' => [[
- 'keyname' => 'tags',
- 'field_type' => 'multi_string',
- 'name' => 'Reading Days',
- 'default' => 'alpha,beta',
- ]]
- ],
- 'configuration' => ['tags' => 'alpha,beta']
- ]);
-
- Volt::test('plugins.config-modal', ['plugin' => $plugin])
- ->assertSet('multiValues.tags', ['alpha', 'beta']);
-});
-
-test('config modal validates against commas in multi_string boxes', function (): void {
- $user = User::factory()->create();
- $this->actingAs($user);
-
- $plugin = Plugin::create([
- 'uuid' => Str::uuid(),
- 'user_id' => $user->id,
- 'name' => 'Test Plugin',
- 'data_strategy' => 'static',
- 'configuration_template' => [
- 'custom_fields' => [[
- 'keyname' => 'tags',
- 'field_type' => 'multi_string',
- 'name' => 'Reading Days',
- ]]
- ]
- ]);
-
- Volt::test('plugins.config-modal', ['plugin' => $plugin])
- ->set('multiValues.tags.0', 'no,commas,allowed')
- ->call('saveConfiguration')
- ->assertHasErrors(['multiValues.tags.0' => 'regex']);
-
- // Assert DB remains unchanged
- expect($plugin->fresh()->configuration['tags'] ?? '')->not->toBe('no,commas,allowed');
-});
-
-test('config modal merges multi_string boxes into a single CSV string on save', function (): void {
- $user = User::factory()->create();
- $this->actingAs($user);
-
- $plugin = Plugin::create([
- 'uuid' => Str::uuid(),
- 'user_id' => $user->id,
- 'name' => 'Test Plugin',
- 'data_strategy' => 'static',
- 'configuration_template' => [
- 'custom_fields' => [[
- 'keyname' => 'items',
- 'field_type' => 'multi_string',
- 'name' => 'Reading Days',
- ]]
- ],
- 'configuration' => []
- ]);
-
- Volt::test('plugins.config-modal', ['plugin' => $plugin])
- ->set('multiValues.items.0', 'First')
- ->call('addMultiItem', 'items')
- ->set('multiValues.items.1', 'Second')
- ->call('saveConfiguration')
- ->assertHasNoErrors();
-
- expect($plugin->fresh()->configuration['items'])->toBe('First,Second');
-});
-
-test('config modal resetForm clears dirty state and increments resetIndex', function (): void {
- $user = User::factory()->create();
- $this->actingAs($user);
-
- $plugin = Plugin::create([
- 'uuid' => Str::uuid(),
- 'user_id' => $user->id,
- 'name' => 'Test Plugin',
- 'data_strategy' => 'static',
- 'configuration' => ['simple_key' => 'original_value']
- ]);
-
- Volt::test('plugins.config-modal', ['plugin' => $plugin])
- ->set('configuration.simple_key', 'dirty_value')
- ->call('resetForm')
- ->assertSet('configuration.simple_key', 'original_value')
- ->assertSet('resetIndex', 1);
-});
-
-test('config modal dispatches update event for parent warning refresh', function (): void {
- $user = User::factory()->create();
- $this->actingAs($user);
-
- $plugin = Plugin::create([
- 'uuid' => Str::uuid(),
- 'user_id' => $user->id,
- 'name' => 'Test Plugin',
- 'data_strategy' => 'static'
- ]);
-
- Volt::test('plugins.config-modal', ['plugin' => $plugin])
- ->call('saveConfiguration')
- ->assertDispatched('config-updated');
-});
diff --git a/tests/Feature/Livewire/Plugins/RecipeSettingsTest.php b/tests/Feature/Livewire/Plugins/RecipeSettingsTest.php
deleted file mode 100644
index a04815f..0000000
--- a/tests/Feature/Livewire/Plugins/RecipeSettingsTest.php
+++ /dev/null
@@ -1,112 +0,0 @@
-create();
- $this->actingAs($user);
-
- $plugin = Plugin::factory()->create([
- 'user_id' => $user->id,
- 'trmnlp_id' => null,
- ]);
-
- $trmnlpId = (string) Str::uuid();
-
- Volt::test('plugins.recipes.settings', ['plugin' => $plugin])
- ->set('trmnlp_id', $trmnlpId)
- ->call('saveTrmnlpId')
- ->assertHasNoErrors();
-
- expect($plugin->fresh()->trmnlp_id)->toBe($trmnlpId);
-});
-
-test('recipe settings validates trmnlp_id is unique per user', function (): void {
- $user = User::factory()->create();
- $this->actingAs($user);
-
- $existingPlugin = Plugin::factory()->create([
- 'user_id' => $user->id,
- 'trmnlp_id' => 'existing-id-123',
- ]);
-
- $newPlugin = Plugin::factory()->create([
- 'user_id' => $user->id,
- 'trmnlp_id' => null,
- ]);
-
- Volt::test('plugins.recipes.settings', ['plugin' => $newPlugin])
- ->set('trmnlp_id', 'existing-id-123')
- ->call('saveTrmnlpId')
- ->assertHasErrors(['trmnlp_id' => 'unique']);
-
- expect($newPlugin->fresh()->trmnlp_id)->toBeNull();
-});
-
-test('recipe settings allows same trmnlp_id for different users', function (): void {
- $user1 = User::factory()->create();
- $user2 = User::factory()->create();
-
- $plugin1 = Plugin::factory()->create([
- 'user_id' => $user1->id,
- 'trmnlp_id' => 'shared-id-123',
- ]);
-
- $plugin2 = Plugin::factory()->create([
- 'user_id' => $user2->id,
- 'trmnlp_id' => null,
- ]);
-
- $this->actingAs($user2);
-
- Volt::test('plugins.recipes.settings', ['plugin' => $plugin2])
- ->set('trmnlp_id', 'shared-id-123')
- ->call('saveTrmnlpId')
- ->assertHasNoErrors();
-
- expect($plugin2->fresh()->trmnlp_id)->toBe('shared-id-123');
-});
-
-test('recipe settings allows same trmnlp_id for the same plugin', function (): void {
- $user = User::factory()->create();
- $this->actingAs($user);
-
- $trmnlpId = (string) Str::uuid();
-
- $plugin = Plugin::factory()->create([
- 'user_id' => $user->id,
- 'trmnlp_id' => $trmnlpId,
- ]);
-
- Volt::test('plugins.recipes.settings', ['plugin' => $plugin])
- ->set('trmnlp_id', $trmnlpId)
- ->call('saveTrmnlpId')
- ->assertHasNoErrors();
-
- expect($plugin->fresh()->trmnlp_id)->toBe($trmnlpId);
-});
-
-test('recipe settings can clear trmnlp_id', function (): void {
- $user = User::factory()->create();
- $this->actingAs($user);
-
- $plugin = Plugin::factory()->create([
- 'user_id' => $user->id,
- 'trmnlp_id' => 'some-id',
- ]);
-
- Volt::test('plugins.recipes.settings', ['plugin' => $plugin])
- ->set('trmnlp_id', '')
- ->call('saveTrmnlpId')
- ->assertHasNoErrors();
-
- expect($plugin->fresh()->trmnlp_id)->toBeNull();
-});
diff --git a/tests/Feature/PlaylistSchedulingTest.php b/tests/Feature/PlaylistSchedulingTest.php
index 18d0032..aea4923 100644
--- a/tests/Feature/PlaylistSchedulingTest.php
+++ b/tests/Feature/PlaylistSchedulingTest.php
@@ -130,48 +130,3 @@ test('playlist isActiveNow handles normal time ranges correctly', function (): v
Carbon::setTestNow(Carbon::create(2024, 1, 1, 20, 0, 0));
expect($playlist->isActiveNow())->toBeFalse();
});
-
-test('playlist scheduling respects user timezone preference', function (): void {
- // Create a user with a timezone that's UTC+1 (e.g., Europe/Berlin)
- // This simulates the bug where setting 00:15 doesn't work until one hour later
- $user = User::factory()->create([
- 'timezone' => 'Europe/Berlin', // UTC+1 in winter, UTC+2 in summer
- ]);
-
- $device = Device::factory()->create(['user_id' => $user->id]);
-
- // Create a playlist that should be active from 00:15 to 01:00 in the user's timezone
- $playlist = Playlist::factory()->create([
- 'device_id' => $device->id,
- 'is_active' => true,
- 'active_from' => '00:15',
- 'active_until' => '01:00',
- 'weekdays' => null,
- ]);
-
- // Set test time to 00:15 in the user's timezone (Europe/Berlin)
- // In January, Europe/Berlin is UTC+1, so 00:15 Berlin time = 23:15 UTC the previous day
- // But Carbon::setTestNow uses UTC by default, so we need to set it to the UTC equivalent
- // For January 1, 2024 at 00:15 Berlin time (UTC+1), that's December 31, 2023 at 23:15 UTC
- $berlinTime = Carbon::create(2024, 1, 1, 0, 15, 0, 'Europe/Berlin');
- Carbon::setTestNow($berlinTime->utc());
-
- // The playlist should be active at 00:15 in the user's timezone
- // This test should pass after the fix, but will fail with the current bug
- expect($playlist->isActiveNow())->toBeTrue();
-
- // Test at 00:30 in user's timezone - should still be active
- $berlinTime = Carbon::create(2024, 1, 1, 0, 30, 0, 'Europe/Berlin');
- Carbon::setTestNow($berlinTime->utc());
- expect($playlist->isActiveNow())->toBeTrue();
-
- // Test at 01:15 in user's timezone - should NOT be active (past the end time)
- $berlinTime = Carbon::create(2024, 1, 1, 1, 15, 0, 'Europe/Berlin');
- Carbon::setTestNow($berlinTime->utc());
- expect($playlist->isActiveNow())->toBeFalse();
-
- // Test at 00:10 in user's timezone - should NOT be active (before start time)
- $berlinTime = Carbon::create(2024, 1, 1, 0, 10, 0, 'Europe/Berlin');
- Carbon::setTestNow($berlinTime->utc());
- expect($playlist->isActiveNow())->toBeFalse();
-});
diff --git a/tests/Feature/PluginImportTest.php b/tests/Feature/PluginImportTest.php
index f3ef1fa..1b20f93 100644
--- a/tests/Feature/PluginImportTest.php
+++ b/tests/Feature/PluginImportTest.php
@@ -83,34 +83,19 @@ it('throws exception for invalid zip file', function (): void {
->toThrow(Exception::class, 'Could not open the ZIP file.');
});
-it('throws exception for missing settings.yml', function (): void {
- $user = User::factory()->create();
-
- $zipContent = createMockZipFile([
- 'src/full.liquid' => getValidFullLiquid(),
- // Missing settings.yml
- ]);
-
- $zipFile = UploadedFile::fake()->createWithContent('test-plugin.zip', $zipContent);
-
- $pluginImportService = new PluginImportService();
- expect(fn (): Plugin => $pluginImportService->importFromZip($zipFile, $user))
- ->toThrow(Exception::class, 'Invalid ZIP structure. Required file settings.yml is missing.');
-});
-
-it('throws exception for missing template files', function (): void {
+it('throws exception for missing required files', function (): void {
$user = User::factory()->create();
$zipContent = createMockZipFile([
'src/settings.yml' => getValidSettingsYaml(),
- // Missing all template files
+ // Missing full.liquid
]);
$zipFile = UploadedFile::fake()->createWithContent('test-plugin.zip', $zipContent);
$pluginImportService = new PluginImportService();
expect(fn (): Plugin => $pluginImportService->importFromZip($zipFile, $user))
- ->toThrow(Exception::class, 'Invalid ZIP structure. At least one of the following files is required: full.liquid, full.blade.php, shared.liquid, or shared.blade.php.');
+ ->toThrow(Exception::class, 'Invalid ZIP structure. Required files settings.yml and full.liquid are missing.');
});
it('sets default values when settings are missing', function (): void {
@@ -442,103 +427,6 @@ YAML;
->and($displayIncidentField['default'])->toBe('true');
});
-it('throws exception when multi_string default value contains a comma', function (): void {
- $user = User::factory()->create();
-
- // YAML with a comma in the 'default' field of a multi_string
- $invalidYaml = <<<'YAML'
-name: Test Plugin
-refresh_interval: 30
-strategy: static
-polling_verb: get
-static_data: '{"test": "data"}'
-custom_fields:
- - keyname: api_key
- field_type: multi_string
- default: default-api-key1,default-api-key2
- label: API Key
-YAML;
-
- $zipContent = createMockZipFile([
- 'src/settings.yml' => $invalidYaml,
- 'src/full.liquid' => getValidFullLiquid(),
- ]);
-
- $zipFile = UploadedFile::fake()->createWithContent('invalid-default.zip', $zipContent);
- $pluginImportService = new PluginImportService();
-
- expect(fn () => $pluginImportService->importFromZip($zipFile, $user))
- ->toThrow(Exception::class, 'Validation Error: The default value for multistring fields like `api_key` cannot contain commas.');
-});
-
-it('throws exception when multi_string placeholder contains a comma', function (): void {
- $user = User::factory()->create();
-
- // YAML with a comma in the 'placeholder' field
- $invalidYaml = <<<'YAML'
-name: Test Plugin
-refresh_interval: 30
-strategy: static
-polling_verb: get
-static_data: '{"test": "data"}'
-custom_fields:
- - keyname: api_key
- field_type: multi_string
- default: default-api-key
- label: API Key
- placeholder: "value1, value2"
-YAML;
-
- $zipContent = createMockZipFile([
- 'src/settings.yml' => $invalidYaml,
- 'src/full.liquid' => getValidFullLiquid(),
- ]);
-
- $zipFile = UploadedFile::fake()->createWithContent('invalid-placeholder.zip', $zipContent);
- $pluginImportService = new PluginImportService();
-
- expect(fn () => $pluginImportService->importFromZip($zipFile, $user))
- ->toThrow(Exception::class, 'Validation Error: The placeholder value for multistring fields like `api_key` cannot contain commas.');
-});
-
-it('imports plugin with only shared.liquid file', function (): void {
- $user = User::factory()->create();
-
- $zipContent = createMockZipFile([
- 'src/settings.yml' => getValidSettingsYaml(),
- 'src/shared.liquid' => '{{ data.title }}
',
- ]);
-
- $zipFile = UploadedFile::fake()->createWithContent('test-plugin.zip', $zipContent);
-
- $pluginImportService = new PluginImportService();
- $plugin = $pluginImportService->importFromZip($zipFile, $user);
-
- expect($plugin)->toBeInstanceOf(Plugin::class)
- ->and($plugin->markup_language)->toBe('liquid')
- ->and($plugin->render_markup)->toContain('')
- ->and($plugin->render_markup)->toContain('
{{ data.title }}
');
-});
-
-it('imports plugin with only shared.blade.php file', function (): void {
- $user = User::factory()->create();
-
- $zipContent = createMockZipFile([
- 'src/settings.yml' => getValidSettingsYaml(),
- 'src/shared.blade.php' => '
{{ $data["title"] }}
',
- ]);
-
- $zipFile = UploadedFile::fake()->createWithContent('test-plugin.zip', $zipContent);
-
- $pluginImportService = new PluginImportService();
- $plugin = $pluginImportService->importFromZip($zipFile, $user);
-
- expect($plugin)->toBeInstanceOf(Plugin::class)
- ->and($plugin->markup_language)->toBe('blade')
- ->and($plugin->render_markup)->toBe('
{{ $data["title"] }}
')
- ->and($plugin->render_markup)->not->toContain('
');
-});
-
// Helper methods
function createMockZipFile(array $files): string
{
diff --git a/tests/Feature/Volt/CatalogTrmnlTest.php b/tests/Feature/Volt/CatalogTrmnlTest.php
index a80c63a..ba1b722 100644
--- a/tests/Feature/Volt/CatalogTrmnlTest.php
+++ b/tests/Feature/Volt/CatalogTrmnlTest.php
@@ -28,33 +28,9 @@ it('loads newest TRMNL recipes on mount', function (): void {
Volt::test('catalog.trmnl')
->assertSee('Weather Chum')
->assertSee('Install')
- ->assertDontSeeHtml('variant="subtle" icon="eye"')
->assertSee('Installs: 10');
});
-it('shows preview button when screenshot_url is provided', function (): void {
- Http::fake([
- 'usetrmnl.com/recipes.json*' => Http::response([
- 'data' => [
- [
- 'id' => 123,
- 'name' => 'Weather Chum',
- 'icon_url' => 'https://example.com/icon.png',
- 'screenshot_url' => 'https://example.com/screenshot.png',
- 'author_bio' => null,
- 'stats' => ['installs' => 10, 'forks' => 2],
- ],
- ],
- ], 200),
- ]);
-
- Livewire::withoutLazyLoading();
-
- Volt::test('catalog.trmnl')
- ->assertSee('Weather Chum')
- ->assertSee('Preview');
-});
-
it('searches TRMNL recipes when search term is provided', function (): void {
Http::fake([
// First call (mount -> newest)
@@ -176,111 +152,3 @@ it('shows error when plugin installation fails', function (): void {
->call('installPlugin', '123')
->assertSee('Error installing plugin'); // This will fail because the zip content is invalid
});
-
-it('previews a recipe with async fetch', function (): void {
- Http::fake([
- 'usetrmnl.com/recipes.json*' => Http::response([
- 'data' => [
- [
- 'id' => 123,
- 'name' => 'Weather Chum',
- 'icon_url' => 'https://example.com/icon.png',
- 'screenshot_url' => 'https://example.com/old.png',
- 'author_bio' => null,
- 'stats' => ['installs' => 10, 'forks' => 2],
- ],
- ],
- ], 200),
- 'usetrmnl.com/recipes/123.json' => Http::response([
- 'data' => [
- 'id' => 123,
- 'name' => 'Weather Chum Updated',
- 'icon_url' => 'https://example.com/icon.png',
- 'screenshot_url' => 'https://example.com/new.png',
- 'author_bio' => ['description' => 'New bio'],
- 'stats' => ['installs' => 11, 'forks' => 3],
- ],
- ], 200),
- ]);
-
- Livewire::withoutLazyLoading();
-
- Volt::test('catalog.trmnl')
- ->assertSee('Weather Chum')
- ->call('previewRecipe', '123')
- ->assertSet('previewingRecipe', '123')
- ->assertSet('previewData.name', 'Weather Chum Updated')
- ->assertSet('previewData.screenshot_url', 'https://example.com/new.png')
- ->assertSee('Preview Weather Chum Updated')
- ->assertSee('New bio');
-});
-
-it('supports pagination and loading more recipes', function (): void {
- Http::fake([
- 'usetrmnl.com/recipes.json?sort-by=newest&page=1' => Http::response([
- 'data' => [
- [
- 'id' => 1,
- 'name' => 'Recipe Page 1',
- 'icon_url' => null,
- 'screenshot_url' => null,
- 'author_bio' => null,
- 'stats' => ['installs' => 1, 'forks' => 0],
- ],
- ],
- 'next_page_url' => '/recipes.json?page=2',
- ], 200),
- 'usetrmnl.com/recipes.json?sort-by=newest&page=2' => Http::response([
- 'data' => [
- [
- 'id' => 2,
- 'name' => 'Recipe Page 2',
- 'icon_url' => null,
- 'screenshot_url' => null,
- 'author_bio' => null,
- 'stats' => ['installs' => 2, 'forks' => 0],
- ],
- ],
- 'next_page_url' => null,
- ], 200),
- ]);
-
- Livewire::withoutLazyLoading();
-
- Volt::test('catalog.trmnl')
- ->assertSee('Recipe Page 1')
- ->assertDontSee('Recipe Page 2')
- ->assertSee('Load next page')
- ->call('loadMore')
- ->assertSee('Recipe Page 1')
- ->assertSee('Recipe Page 2')
- ->assertDontSee('Load next page');
-});
-
-it('resets pagination when search term changes', function (): void {
- Http::fake([
- 'usetrmnl.com/recipes.json?sort-by=newest&page=1' => Http::sequence()
- ->push([
- 'data' => [['id' => 1, 'name' => 'Initial 1']],
- 'next_page_url' => '/recipes.json?page=2',
- ])
- ->push([
- 'data' => [['id' => 3, 'name' => 'Initial 1 Again']],
- 'next_page_url' => null,
- ]),
- 'usetrmnl.com/recipes.json?search=weather&sort-by=newest&page=1' => Http::response([
- 'data' => [['id' => 2, 'name' => 'Weather Result']],
- 'next_page_url' => null,
- ]),
- ]);
-
- Livewire::withoutLazyLoading();
-
- Volt::test('catalog.trmnl')
- ->assertSee('Initial 1')
- ->call('loadMore')
- ->set('search', 'weather')
- ->assertSee('Weather Result')
- ->assertDontSee('Initial 1')
- ->assertSet('page', 1);
-});
diff --git a/tests/Unit/Models/PluginTest.php b/tests/Unit/Models/PluginTest.php
index aa9a28e..cf8ea97 100644
--- a/tests/Unit/Models/PluginTest.php
+++ b/tests/Unit/Models/PluginTest.php
@@ -99,35 +99,6 @@ test('updateDataPayload handles multiple URLs with IDX_ prefixes', function ():
expect($plugin->data_payload['IDX_2'])->toBe(['headline' => 'test']);
});
-test('updateDataPayload skips empty lines in polling_url and maintains sequential IDX keys', function (): void {
- $plugin = Plugin::factory()->create([
- 'data_strategy' => 'polling',
- // empty lines and extra spaces between the URL to generate empty entries
- 'polling_url' => "https://api1.example.com/data\n \n\nhttps://api2.example.com/weather\n ",
- 'polling_verb' => 'get',
- ]);
-
- // Mock only the valid URLs
- Http::fake([
- 'https://api1.example.com/data' => Http::response(['item' => 'first'], 200),
- 'https://api2.example.com/weather' => Http::response(['item' => 'second'], 200),
- ]);
-
- $plugin->updateDataPayload();
-
- // payload should only have 2 items, and they should be indexed 0 and 1
- expect($plugin->data_payload)->toHaveCount(2);
- expect($plugin->data_payload)->toHaveKey('IDX_0');
- expect($plugin->data_payload)->toHaveKey('IDX_1');
-
- // data is correct
- expect($plugin->data_payload['IDX_0'])->toBe(['item' => 'first']);
- expect($plugin->data_payload['IDX_1'])->toBe(['item' => 'second']);
-
- // no empty index exists
- expect($plugin->data_payload)->not->toHaveKey('IDX_2');
-});
-
test('updateDataPayload handles single URL without nesting', function (): void {
$plugin = Plugin::factory()->create([
'data_strategy' => 'polling',
@@ -708,233 +679,3 @@ test('plugin render includes utc_offset and time_zone_iana in trmnl.user context
->toContain('America/Chicago')
->and($rendered)->toMatch('/\|-?\d+/'); // Should contain a pipe followed by a number (offset in seconds)
});
-
-/**
- * Plugin security: XSS Payload Dataset
- * [Input, Expected Result, Forbidden String]
- */
-dataset('xss_vectors', [
- 'standard_script' => ['Safe ', 'Safe ', '