fix(#124): improve timezone support
Some checks failed
tests / ci (push) Has been cancelled

This commit is contained in:
Benjamin Nussbaum 2025-12-02 16:44:41 +01:00
parent 0322ec899e
commit b10bbca774
9 changed files with 168 additions and 12 deletions

View file

@ -121,6 +121,10 @@ class GenerateDefaultImagesCommand extends Command
$browserStage = new BrowserStage($browsershotInstance);
$browserStage->html($html);
// Set timezone from app config (no user context in this command)
$browserStage->timezone(config('app.timezone'));
$browserStage
->width($deviceModel->width)
->height($deviceModel->height);

View file

@ -12,6 +12,7 @@ use App\Liquid\Filters\StringMarkup;
use App\Liquid\Filters\Uniqueness;
use App\Liquid\Tags\TemplateTag;
use App\Services\PluginImportService;
use Carbon\Carbon;
use Exception;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
@ -454,6 +455,12 @@ class Plugin extends Model
$renderedContent = '';
if ($this->markup_language === 'liquid') {
// Get timezone from user or fall back to app timezone
$timezone = $this->user->timezone ?? config('app.timezone');
// Calculate UTC offset in seconds
$utcOffset = (string) Carbon::now($timezone)->getOffset();
// Build render context
$context = [
'size' => $size,
@ -465,10 +472,10 @@ class Plugin extends Model
'timestamp_utc' => now()->utc()->timestamp,
],
'user' => [
'utc_offset' => '0',
'utc_offset' => $utcOffset,
'name' => $this->user->name ?? 'Unknown User',
'locale' => 'en',
'time_zone_iana' => config('app.timezone'),
'time_zone_iana' => $timezone,
],
'plugin_settings' => [
'instance_name' => $this->name,

View file

@ -27,6 +27,7 @@ class User extends Authenticatable // implements MustVerifyEmail
'assign_new_devices',
'assign_new_device_id',
'oidc_sub',
'timezone',
];
/**

View file

@ -25,7 +25,7 @@ class ImageGenerationService
{
public static function generateImage(string $markup, $deviceId): string
{
$device = Device::with(['deviceModel', 'palette', 'deviceModel.palette'])->find($deviceId);
$device = Device::with(['deviceModel', 'palette', 'deviceModel.palette', 'user'])->find($deviceId);
$uuid = Uuid::uuid4()->toString();
try {
@ -44,6 +44,10 @@ class ImageGenerationService
$browserStage = new BrowserStage($browsershotInstance);
$browserStage->html($markup);
// Set timezone from user or fall back to app timezone
$timezone = $device->user->timezone ?? config('app.timezone');
$browserStage->timezone($timezone);
if (config('app.puppeteer_window_size_strategy') === 'v2') {
$browserStage
->width($imageSettings['width'])
@ -352,7 +356,7 @@ class ImageGenerationService
try {
// Load device with relationships
$device->load(['palette', 'deviceModel.palette']);
$device->load(['palette', 'deviceModel.palette', 'user']);
// Get image generation settings from DeviceModel if available, otherwise use device settings
$imageSettings = self::getImageSettings($device);
@ -372,6 +376,10 @@ class ImageGenerationService
$browserStage = new BrowserStage($browsershotInstance);
$browserStage->html($html);
// Set timezone from user or fall back to app timezone
$timezone = $device->user->timezone ?? config('app.timezone');
$browserStage->timezone($timezone);
if (config('app.puppeteer_window_size_strategy') === 'v2') {
$browserStage
->width($imageSettings['width'])

View file

@ -15,7 +15,7 @@
"ext-simplexml": "*",
"ext-zip": "*",
"bnussbau/laravel-trmnl-blade": "2.0.*",
"bnussbau/trmnl-pipeline-php": "^0.5.0",
"bnussbau/trmnl-pipeline-php": "^0.6.0",
"keepsuit/laravel-liquid": "^0.5.2",
"laravel/framework": "^12.1",
"laravel/sanctum": "^4.0",

14
composer.lock generated
View file

@ -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": "38e8a7dd90ccc1b777a4c8a5a28f9f14",
"content-hash": "7750ff686c4cad7f85390488c28b33ca",
"packages": [
{
"name": "aws/aws-crt-php",
@ -243,16 +243,16 @@
},
{
"name": "bnussbau/trmnl-pipeline-php",
"version": "0.5.0",
"version": "0.6.0",
"source": {
"type": "git",
"url": "https://github.com/bnussbau/trmnl-pipeline-php.git",
"reference": "eb55b89e1f3991764912505872bbce809874d1aa"
"reference": "228505afa8a39a5033916e9ae5ccbf1733092c1f"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/bnussbau/trmnl-pipeline-php/zipball/eb55b89e1f3991764912505872bbce809874d1aa",
"reference": "eb55b89e1f3991764912505872bbce809874d1aa",
"url": "https://api.github.com/repos/bnussbau/trmnl-pipeline-php/zipball/228505afa8a39a5033916e9ae5ccbf1733092c1f",
"reference": "228505afa8a39a5033916e9ae5ccbf1733092c1f",
"shasum": ""
},
"require": {
@ -294,7 +294,7 @@
],
"support": {
"issues": "https://github.com/bnussbau/trmnl-pipeline-php/issues",
"source": "https://github.com/bnussbau/trmnl-pipeline-php/tree/0.5.0"
"source": "https://github.com/bnussbau/trmnl-pipeline-php/tree/0.6.0"
},
"funding": [
{
@ -310,7 +310,7 @@
"type": "github"
}
],
"time": "2025-11-25T17:00:21+00:00"
"time": "2025-12-02T15:18:51+00:00"
},
{
"name": "brick/math",

View file

@ -0,0 +1,28 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('users', function (Blueprint $table) {
$table->string('timezone')->nullable()->after('oidc_sub');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('users', function (Blueprint $table) {
$table->dropColumn('timezone');
});
}
};

View file

@ -11,9 +11,12 @@ use Livewire\Volt\Component;
new class extends Component {
public ?int $assign_new_device_id = null;
public ?string $timezone = null;
public function mount(): void
{
$this->assign_new_device_id = Auth::user()->assign_new_device_id;
$this->timezone = Auth::user()->timezone ?? config('app.timezone');
}
public function updatePreferences(): void
@ -26,6 +29,11 @@ new class extends Component {
->whereNull('mirror_device_id');
}),
],
'timezone' => [
'nullable',
'string',
Rule::in(timezone_identifiers_list()),
],
]);
Auth::user()->update($validated);
@ -39,6 +47,14 @@ new class extends Component {
<x-settings.layout heading="Preferences" subheading="Update your preferences">
<form wire:submit="updatePreferences" class="my-6 w-full space-y-6">
<flux:select wire:model="timezone" label="Timezone">
<flux:select.option value="" disabled>Select timezone...</flux:select.option>
@foreach(timezone_identifiers_list() as $tz)
<flux:select.option value="{{ $tz }}">{{ $tz }}</flux:select.option>
@endforeach
</flux:select>
<flux:select wire:model="assign_new_device_id" label="Auto-Joined Devices should mirror">
<flux:select.option value="">None</flux:select.option>
@foreach(auth()->user()->devices->where('mirror_device_id', null) as $device)

View file

@ -1,6 +1,8 @@
<?php
use App\Models\Plugin;
use App\Models\User;
use Carbon\Carbon;
use Illuminate\Support\Facades\Http;
uses(Illuminate\Foundation\Testing\RefreshDatabase::class);
@ -587,3 +589,93 @@ LIQUID
return str_contains($command, 'trmnl-liquid-cli');
});
});
test('plugin render uses user timezone when set', function (): void {
$user = User::factory()->create([
'timezone' => 'America/New_York',
]);
$plugin = Plugin::factory()->create([
'user_id' => $user->id,
'markup_language' => 'liquid',
'render_markup' => '{{ trmnl.user.time_zone_iana }}',
]);
$rendered = $plugin->render();
expect($rendered)->toContain('America/New_York');
});
test('plugin render falls back to app timezone when user timezone is not set', function (): void {
$user = User::factory()->create([
'timezone' => null,
]);
config(['app.timezone' => 'Europe/London']);
$plugin = Plugin::factory()->create([
'user_id' => $user->id,
'markup_language' => 'liquid',
'render_markup' => '{{ trmnl.user.time_zone_iana }}',
]);
$rendered = $plugin->render();
expect($rendered)->toContain('Europe/London');
});
test('plugin render calculates correct UTC offset from user timezone', function (): void {
$user = User::factory()->create([
'timezone' => 'America/New_York', // UTC-5 (EST) or UTC-4 (EDT)
]);
$plugin = Plugin::factory()->create([
'user_id' => $user->id,
'markup_language' => 'liquid',
'render_markup' => '{{ trmnl.user.utc_offset }}',
]);
$rendered = $plugin->render();
// America/New_York offset should be -18000 (EST) or -14400 (EDT) in seconds
$expectedOffset = (string) Carbon::now('America/New_York')->getOffset();
expect($rendered)->toContain($expectedOffset);
});
test('plugin render calculates correct UTC offset from app timezone when user timezone is null', function (): void {
$user = User::factory()->create([
'timezone' => null,
]);
config(['app.timezone' => 'Europe/London']);
$plugin = Plugin::factory()->create([
'user_id' => $user->id,
'markup_language' => 'liquid',
'render_markup' => '{{ trmnl.user.utc_offset }}',
]);
$rendered = $plugin->render();
// Europe/London offset should be 0 (GMT) or 3600 (BST) in seconds
$expectedOffset = (string) Carbon::now('Europe/London')->getOffset();
expect($rendered)->toContain($expectedOffset);
});
test('plugin render includes utc_offset and time_zone_iana in trmnl.user context', function (): void {
$user = User::factory()->create([
'timezone' => 'America/Chicago', // UTC-6 (CST) or UTC-5 (CDT)
]);
$plugin = Plugin::factory()->create([
'user_id' => $user->id,
'markup_language' => 'liquid',
'render_markup' => '{{ trmnl.user.time_zone_iana }}|{{ trmnl.user.utc_offset }}',
]);
$rendered = $plugin->render();
expect($rendered)
->toContain('America/Chicago')
->and($rendered)->toMatch('/\|-?\d+/'); // Should contain a pipe followed by a number (offset in seconds)
});