feat(#16): refactor

This commit is contained in:
Benjamin Nussbaum 2025-05-12 07:19:03 +02:00
parent 580a5833a8
commit cc63c8cce2
9 changed files with 148 additions and 75 deletions

View file

@ -36,8 +36,7 @@ class ScreenGeneratorCommand extends Command
return 1;
}
GenerateScreenJob::dispatchSync($deviceId, $markup);
GenerateScreenJob::dispatchSync($deviceId, null, $markup);
$this->info('Screen generation job finished.');

View file

@ -1,37 +0,0 @@
<?php
namespace App\Jobs;
use App\Models\Plugin;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
class GeneratePluginJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
/**
* Create a new job instance.
*/
public function __construct(
private readonly int $pluginId,
private readonly string $markup
) {}
/**
* Execute the job.
*/
public function handle(): void
{
$newImageUuid = CommonFunctions::generateImage($this->markup);
Plugin::find($this->pluginId)->update(['current_image' => $newImageUuid]);
\Log::info("Plugin $this->pluginId: updated with new image: $newImageUuid");
CommonFunctions::cleanupFolder();
}
}

View file

@ -3,6 +3,8 @@
namespace App\Jobs;
use App\Models\Device;
use App\Models\Plugin;
use App\Services\ImageGenerationService;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
@ -18,6 +20,7 @@ class GenerateScreenJob implements ShouldQueue
*/
public function __construct(
private readonly int $deviceId,
private readonly ?int $pluginId,
private readonly string $markup
) {}
@ -26,11 +29,16 @@ class GenerateScreenJob implements ShouldQueue
*/
public function handle(): void
{
$newImageUuid = CommonFunctions::generateImage($this->markup);
$newImageUuid = ImageGenerationService::generateImage($this->markup);
Device::find($this->deviceId)->update(['current_screen_image' => $newImageUuid]);
\Log::info("Device $this->deviceId: updated with new image: $newImageUuid");
CommonFunctions::cleanupFolder();
if ($this->pluginId) {
// cache current image
Plugin::find($this->pluginId)->update(['current_image' => $newImageUuid]);
}
ImageGenerationService::cleanupFolder();
}
}

View file

@ -17,7 +17,6 @@ class Plugin extends Model
'data_payload' => 'json',
'data_payload_updated_at' => 'datetime',
'is_native' => 'boolean',
'current_image' => 'string',
];
protected static function boot()

View file

@ -1,6 +1,6 @@
<?php
namespace App\Jobs;
namespace App\Services;
use App\Models\Device;
use App\Models\Plugin;
@ -9,7 +9,7 @@ use Ramsey\Uuid\Uuid;
use Spatie\Browsershot\Browsershot;
use Wnx\SidecarBrowsershot\BrowsershotLambda;
class CommonFunctions
class ImageGenerationService
{
public static function generateImage(string $markup): string {
$uuid = Uuid::uuid4()->toString();
@ -37,7 +37,7 @@ class CommonFunctions
}
try {
CommonFunctions::convertToBmpImageMagick($pngPath, $bmpPath);
ImageGenerationService::convertToBmpImageMagick($pngPath, $bmpPath);
} catch (\ImagickException $e) {
throw new \RuntimeException('Failed to convert image to BMP: '.$e->getMessage(), 0, $e);
}

View file

@ -33,7 +33,7 @@ new class extends Component {
try {
$rendered = Blade::render($this->blade_code);
foreach ($this->checked_devices as $device) {
GenerateScreenJob::dispatchSync($device, $rendered);
GenerateScreenJob::dispatchSync($device, null, $rendered);
}
} catch (\Exception $e) {
$this->addError('error', $e->getMessage());

View file

@ -1,7 +1,6 @@
<?php
use App\Jobs\GenerateScreenJob;
use App\Jobs\GeneratePluginJob;
use App\Models\Device;
use App\Models\User;
use Illuminate\Http\Request;
@ -47,11 +46,12 @@ Route::get('/display', function (Request $request) {
// Get current screen image from mirror device or continue if not available
if (! $image_uuid = $device->mirrorDevice?->current_screen_image) {
$refreshTimeOverride = null;
$nextPlaylistItem = $device->getNextPlaylistItem();
// Skip if cloud proxy is enabled for the device
if (! $device->proxy_cloud && $nextPlaylistItem) {
$refreshTimeOverride = $nextPlaylistItem->playlist()->first()->refresh_time;
$plugin = $nextPlaylistItem->plugin;
if (! $device->proxy_cloud || $device->getNextPlaylistItem()) {
$playlistItem = $device->getNextPlaylistItem();
if ($playlistItem) {
$refreshTimeOverride = $playlistItem->playlist()->first()->refresh_time;
$plugin = $playlistItem->plugin;
// Check and update stale data if needed
if ($plugin->isDataStale() || $plugin->current_image == null) {
@ -63,17 +63,17 @@ Route::get('/display', function (Request $request) {
$markup = view($plugin->render_markup_view, ['data' => $plugin->data_payload])->render();
}
GeneratePluginJob::dispatchSync($plugin->id, $markup);
GenerateScreenJob::dispatchSync($device->id, $plugin->id, $markup);
}
$plugin->refresh();
if ($plugin->current_image != null)
{
$nextPlaylistItem->update(['last_displayed_at' => now()]);
if ($plugin->current_image != null) {
$playlistItem->update(['last_displayed_at' => now()]);
$device->update(['current_screen_image' => $plugin->current_image]);
}
}
}
$device->refresh();
$image_uuid = $device->current_screen_image;
@ -198,11 +198,11 @@ Route::get('/devices', function (Request $request) {
'friendly_id',
'mac_address',
'last_battery_voltage as battery_voltage',
'last_rssi_level as rssi'
'last_rssi_level as rssi',
]);
return response()->json([
'data' => $devices
'data' => $devices,
]);
})->middleware('auth:sanctum');
@ -217,7 +217,7 @@ Route::post('/display/update', function (Request $request) {
$view = Blade::render($request['markup']);
GenerateScreenJob::dispatchSync($deviceId, $view);
GenerateScreenJob::dispatchSync($deviceId, null, $view);
response()->json([
'message' => 'success',

View file

@ -469,7 +469,7 @@ test('authenticated user can fetch their devices', function () {
]);
});
test('plugin doesn\'t update image unless required', function () {
test('plugin caches image until data is stale', function () {
// Create source device with a playlist
$device = Device::factory()->create([
'mac_address' => '55:11:22:33:44:55',
@ -479,11 +479,11 @@ test('plugin doesn\'t update image unless required', function () {
$plugin = Plugin::factory()->create([
'name' => 'Zen Quotes',
'polling_url' => 'https://zenquotes.io/api/today',
'polling_url' => null,
'data_stale_minutes' => 1,
'data_strategy' => 'polling',
'polling_verb' => 'get',
'render_markup_view' => 'recipes.zen',
'render_markup_view' => 'trmnl',
'is_native' => false,
'data_payload_updated_at' => null,
]);
@ -543,3 +543,107 @@ test('plugin doesn\'t update image unless required', function () {
expect($thirdResponse['filename'])
->not->toBe($firstResponse['filename']);
})->skipOnGitHubActions();
test('plugins in playlist are rendered in order', function () {
// Create source device with a playlist
$device = Device::factory()->create([
'mac_address' => '55:11:22:33:44:55',
'api_key' => 'source-api-key',
'proxy_cloud' => true,
]);
// Create two plugins
$firstPlugin = Plugin::factory()->create([
'name' => 'First Plugin',
'polling_url' => null,
'data_stale_minutes' => 1,
'data_strategy' => 'polling',
'polling_verb' => 'get',
'render_markup_view' => 'trmnl',
'is_native' => false,
'data_payload_updated_at' => null,
]);
$secondPlugin = Plugin::factory()->create([
'name' => 'Second Plugin',
'polling_url' => null,
'data_stale_minutes' => 1,
'data_strategy' => 'polling',
'polling_verb' => 'get',
'render_markup_view' => 'trmnl',
'is_native' => false,
'data_payload_updated_at' => null,
]);
// Create playlist
$playlist = Playlist::factory()->create([
'device_id' => $device->id,
'name' => 'Two Plugins Test',
'is_active' => true,
'weekdays' => null,
'active_from' => null,
'active_until' => null,
]);
// Add plugins to playlist in specific order
PlaylistItem::factory()->create([
'playlist_id' => $playlist->id,
'plugin_id' => $firstPlugin->id,
'order' => 1,
'is_active' => true,
'last_displayed_at' => null,
]);
PlaylistItem::factory()->create([
'playlist_id' => $playlist->id,
'plugin_id' => $secondPlugin->id,
'order' => 2,
'is_active' => true,
'last_displayed_at' => null,
]);
// First request should show the first plugin
$firstResponse = $this->withHeaders([
'id' => $device->mac_address,
'access-token' => $device->api_key,
])->get('/api/display');
$firstResponse->assertOk();
$firstImageFilename = $firstResponse['filename'];
expect($firstImageFilename)->not->toBe('setup-logo.bmp');
// Get the first plugin's playlist item and verify it was marked as displayed
$firstPluginItem = PlaylistItem::where('plugin_id', $firstPlugin->id)->first();
expect($firstPluginItem->last_displayed_at)->not->toBeNull();
// Second request should show the second plugin
$secondResponse = $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');
$secondResponse->assertOk();
expect($secondResponse['filename'])
->not->toBe($firstImageFilename)
->not->toBe('setup-logo.bmp');
// Get the second plugin's playlist item and verify it was marked as displayed
$secondPluginItem = PlaylistItem::where('plugin_id', $secondPlugin->id)->first();
expect($secondPluginItem->last_displayed_at)->not->toBeNull();
// Third request should show the first plugin again
$thirdResponse = $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');
$thirdResponse->assertOk();
expect($thirdResponse['filename'])
->not->toBe($secondResponse['filename']);
})->skipOnGitHubActions();

View file

@ -13,7 +13,7 @@ beforeEach(function () {
test('it generates screen images and updates device', function () {
$device = Device::factory()->create();
$job = new GenerateScreenJob($device->id, view('trmnl')->render());
$job = new GenerateScreenJob($device->id, null, view('trmnl')->render());
$job->handle();
// Assert the device was updated with a new image UUID
@ -39,7 +39,7 @@ test('it cleans up unused images', function () {
Storage::disk('public')->put('/images/generated/inactive-uuid.bmp', 'test');
// Run a job which will trigger cleanup
$job = new GenerateScreenJob($activeDevice->id, '<div>Test</div>');
$job = new GenerateScreenJob($activeDevice->id, null, '<div>Test</div>');
$job->handle();
Storage::disk('public')->assertMissing('/images/generated/uuid-to-be-replaced.png');
@ -52,7 +52,7 @@ test('it preserves gitignore file during cleanup', function () {
Storage::disk('public')->put('/images/generated/.gitignore', '*');
$device = Device::factory()->create();
$job = new GenerateScreenJob($device->id, '<div>Test</div>');
$job = new GenerateScreenJob($device->id, null, '<div>Test</div>');
$job->handle();
Storage::disk('public')->assertExists('/images/generated/.gitignore');