Compare commits

...

6 commits
0.25.0 ... main

Author SHA1 Message Date
Benjamin Nussbaum
d7f6237265 fix(#158): do not require ID header on /display endpoint
Some checks are pending
tests / ci (push) Waiting to run
2026-01-13 17:32:12 +01:00
Benjamin Nussbaum
c2c553461f fix: codemirror readonly when render_markup_view is set 2026-01-13 17:06:13 +01:00
Benjamin Nussbaum
fceacfe4b3 fix(#157): migrations and seeders for pgsql 2026-01-13 16:48:48 +01:00
Benjamin Nussbaum
3032c09778 fix: markup for recipe 'Zen Quotes'
Some checks are pending
tests / ci (push) Waiting to run
2026-01-12 17:58:22 +01:00
Benjamin Nussbaum
f1903bcbe8 chore: change button variant 2026-01-12 17:42:25 +01:00
Benjamin Nussbaum
621c108e78 chore: Alias improve wording 2026-01-12 16:32:26 +01:00
7 changed files with 26 additions and 30 deletions

View file

@ -1,8 +1,8 @@
<?php <?php
use App\Models\Plugin;
use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint; use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema; use Illuminate\Support\Facades\Schema;
return new class extends Migration return new class extends Migration
@ -13,16 +13,16 @@ return new class extends Migration
public function up(): void public function up(): void
{ {
// Find and handle duplicate (user_id, trmnlp_id) combinations // Find and handle duplicate (user_id, trmnlp_id) combinations
$duplicates = DB::table('plugins') $duplicates = Plugin::query()
->select('user_id', 'trmnlp_id', DB::raw('COUNT(*) as count')) ->selectRaw('user_id, trmnlp_id, COUNT(*) as duplicate_count')
->whereNotNull('trmnlp_id') ->whereNotNull('trmnlp_id')
->groupBy('user_id', 'trmnlp_id') ->groupBy('user_id', 'trmnlp_id')
->having('count', '>', 1) ->havingRaw('COUNT(*) > ?', [1])
->get(); ->get();
// For each duplicate combination, keep the first one (by id) and set others to null // For each duplicate combination, keep the first one (by id) and set others to null
foreach ($duplicates as $duplicate) { foreach ($duplicates as $duplicate) {
$plugins = DB::table('plugins') $plugins = Plugin::query()
->where('user_id', $duplicate->user_id) ->where('user_id', $duplicate->user_id)
->where('trmnlp_id', $duplicate->trmnlp_id) ->where('trmnlp_id', $duplicate->trmnlp_id)
->orderBy('id') ->orderBy('id')
@ -37,9 +37,7 @@ return new class extends Migration
continue; continue;
} }
DB::table('plugins') $plugin->update(['trmnlp_id' => null]);
->where('id', $plugin->id)
->update(['trmnlp_id' => null]);
} }
} }

View file

@ -13,8 +13,8 @@ class ExampleRecipesSeeder extends Seeder
public function run($user_id = 1): void public function run($user_id = 1): void
{ {
Plugin::updateOrCreate( Plugin::updateOrCreate(
['uuid' => '9e46c6cf-358c-4bfe-8998-436b3a207fec'],
[ [
'uuid' => '9e46c6cf-358c-4bfe-8998-436b3a207fec',
'name' => 'ÖBB Departures', 'name' => 'ÖBB Departures',
'user_id' => $user_id, 'user_id' => $user_id,
'data_payload' => null, 'data_payload' => null,
@ -32,8 +32,8 @@ class ExampleRecipesSeeder extends Seeder
); );
Plugin::updateOrCreate( Plugin::updateOrCreate(
['uuid' => '3b046eda-34e9-4232-b935-c33b989a284b'],
[ [
'uuid' => '3b046eda-34e9-4232-b935-c33b989a284b',
'name' => 'Weather', 'name' => 'Weather',
'user_id' => $user_id, 'user_id' => $user_id,
'data_payload' => null, 'data_payload' => null,
@ -51,8 +51,8 @@ class ExampleRecipesSeeder extends Seeder
); );
Plugin::updateOrCreate( Plugin::updateOrCreate(
['uuid' => '21464b16-5f5a-4099-a967-f5c915e3da54'],
[ [
'uuid' => '21464b16-5f5a-4099-a967-f5c915e3da54',
'name' => 'Zen Quotes', 'name' => 'Zen Quotes',
'user_id' => $user_id, 'user_id' => $user_id,
'data_payload' => null, 'data_payload' => null,
@ -70,8 +70,8 @@ class ExampleRecipesSeeder extends Seeder
); );
Plugin::updateOrCreate( Plugin::updateOrCreate(
['uuid' => '8d472959-400f-46ee-afb2-4a9f1cfd521f'],
[ [
'uuid' => '8d472959-400f-46ee-afb2-4a9f1cfd521f',
'name' => 'This Day in History', 'name' => 'This Day in History',
'user_id' => $user_id, 'user_id' => $user_id,
'data_payload' => null, 'data_payload' => null,
@ -89,8 +89,8 @@ class ExampleRecipesSeeder extends Seeder
); );
Plugin::updateOrCreate( Plugin::updateOrCreate(
['uuid' => '4349fdad-a273-450b-aa00-3d32f2de788d'],
[ [
'uuid' => '4349fdad-a273-450b-aa00-3d32f2de788d',
'name' => 'Home Assistant', 'name' => 'Home Assistant',
'user_id' => $user_id, 'user_id' => $user_id,
'data_payload' => null, 'data_payload' => null,
@ -108,8 +108,8 @@ class ExampleRecipesSeeder extends Seeder
); );
Plugin::updateOrCreate( Plugin::updateOrCreate(
['uuid' => 'be5f7e1f-3ad8-4d66-93b2-36f7d6dcbd80'],
[ [
'uuid' => 'be5f7e1f-3ad8-4d66-93b2-36f7d6dcbd80',
'name' => 'Sunrise/Sunset', 'name' => 'Sunrise/Sunset',
'user_id' => $user_id, 'user_id' => $user_id,
'data_payload' => null, 'data_payload' => null,
@ -127,8 +127,8 @@ class ExampleRecipesSeeder extends Seeder
); );
Plugin::updateOrCreate( Plugin::updateOrCreate(
['uuid' => '82d3ee14-d578-4969-bda5-2bbf825435fe'],
[ [
'uuid' => '82d3ee14-d578-4969-bda5-2bbf825435fe',
'name' => 'Pollen Forecast', 'name' => 'Pollen Forecast',
'user_id' => $user_id, 'user_id' => $user_id,
'data_payload' => null, 'data_payload' => null,
@ -146,8 +146,8 @@ class ExampleRecipesSeeder extends Seeder
); );
Plugin::updateOrCreate( Plugin::updateOrCreate(
['uuid' => '1d98bca4-837d-4b01-b1a1-e3b6e56eca90'],
[ [
'uuid' => '1d98bca4-837d-4b01-b1a1-e3b6e56eca90',
'name' => 'Holidays (iCal)', 'name' => 'Holidays (iCal)',
'user_id' => $user_id, 'user_id' => $user_id,
'data_payload' => null, 'data_payload' => null,

View file

@ -803,7 +803,7 @@ HTML;
</flux:field> </flux:field>
</div> </div>
<flux:button variant="primary" icon="cloud-arrow-down" wire:click="updateData" class="w-full mt-4"> <flux:button icon="cloud-arrow-down" wire:click="updateData" class="w-full mt-4">
Fetch data now Fetch data now
</flux:button> </flux:button>
</div> </div>
@ -950,7 +950,7 @@ HTML;
/> />
<div <div
x-data="codeEditorFormComponent({ x-data="codeEditorFormComponent({
isDisabled: false, isDisabled: @js((bool)$plugin->render_markup_view),
language: 'liquid', language: 'liquid',
state: $wire.entangle('markup_code'), state: $wire.entangle('markup_code'),
textareaId: @js($textareaId) textareaId: @js($textareaId)

View file

@ -76,7 +76,7 @@ new class extends Component {
<flux:field> <flux:field>
<flux:checkbox wire:model.live="alias" label="Enable Alias" /> <flux:checkbox wire:model.live="alias" label="Enable Alias" />
<flux:description>Enable a public alias URL for this recipe.</flux:description> <flux:description>Enable an Alias URL for this recipe. Your server does not need to be exposed to the internet, but your device must be able to reach the URL. <a href="https://help.usetrmnl.com/en/articles/10701448-alias-plugin">Docs</a></flux:description>
</flux:field> </flux:field>
@if($alias) @if($alias)
@ -87,7 +87,7 @@ new class extends Component {
readonly readonly
copyable copyable
/> />
<flux:description>Use this URL to access the recipe image directly. Add <code>?device-model=name</code> to specify a device model.</flux:description> <flux:description>Copy this URL to your TRMNL Dashboard. By default, image is created for TRMNL OG; use parameter <code>?device-model=</code> to specify a device model.</flux:description>
</flux:field> </flux:field>
@endif @endif
</div> </div>

View file

@ -3,11 +3,11 @@
<x-trmnl::view size="{{ $size }}"> <x-trmnl::view size="{{ $size }}">
<x-trmnl::layout> <x-trmnl::layout>
<x-trmnl::layout class="layout--col"> <x-trmnl::layout class="layout--col">
<div class="b-h-gray-1">{{$data[0]['a']}}</div> <div class="b-h-gray-1">{{$data['data'][0]['a'] ?? ''}}</div>
@if (strlen($data[0]['q']) < 300 && $size != 'quadrant') @if (strlen($data['data'][0]['q'] ?? '') < 300 && $size != 'quadrant')
<p class="value">{{ $data[0]['q'] }}</p> <p class="value">{{ $data['data'][0]['q'] ?? '' }}</p>
@else @else
<p class="value--small">{{ $data[0]['q'] }}</p> <p class="value--small">{{ $data['data'][0]['q'] ?? '' }}</p>
@endif @endif
</x-trmnl::layout> </x-trmnl::layout>
</x-trmnl::layout> </x-trmnl::layout>

View file

@ -18,15 +18,13 @@ use Illuminate\Support\Str;
Route::get('/display', function (Request $request) { Route::get('/display', function (Request $request) {
$mac_address = $request->header('id'); $mac_address = $request->header('id');
$access_token = $request->header('access-token'); $access_token = $request->header('access-token');
$device = Device::where('mac_address', mb_strtoupper($mac_address ?? '')) $device = Device::where('api_key', $access_token)->first();
->where('api_key', $access_token)
->first();
if (! $device) { if (! $device) {
// Check if there's a user with assign_new_devices enabled // Check if there's a user with assign_new_devices enabled
$auto_assign_user = User::where('assign_new_devices', true)->first(); $auto_assign_user = User::where('assign_new_devices', true)->first();
if ($auto_assign_user) { if ($auto_assign_user && $mac_address) {
// Create a new device and assign it to this user // Create a new device and assign it to this user
$device = Device::create([ $device = Device::create([
'mac_address' => mb_strtoupper($mac_address ?? ''), 'mac_address' => mb_strtoupper($mac_address ?? ''),
@ -39,7 +37,7 @@ Route::get('/display', function (Request $request) {
]); ]);
} else { } else {
return response()->json([ return response()->json([
'message' => 'MAC Address not registered or invalid access token', 'message' => 'MAC Address not registered (or not set), or invalid access token',
], 404); ], 404);
} }
} }

View file

@ -263,7 +263,7 @@ test('invalid device credentials return error', function (): void {
])->get('/api/display'); ])->get('/api/display');
$response->assertNotFound() $response->assertNotFound()
->assertJson(['message' => 'MAC Address not registered or invalid access token']); ->assertJson(['message' => 'MAC Address not registered (or not set), or invalid access token']);
}); });
test('log endpoint requires valid device credentials', function (): void { test('log endpoint requires valid device credentials', function (): void {