Compare commits

...

4 commits

Author SHA1 Message Date
Benjamin Nussbaum
a86315c5c7 fix: init exception
Some checks failed
tests / ci (push) Has been cancelled
2026-01-10 22:10:37 +01:00
Benjamin Nussbaum
887c4d130b chore: gitignore 2026-01-10 19:55:35 +01:00
Benjamin Nussbaum
74e9e1eba3 chore: update dependencies 2026-01-10 19:54:26 +01:00
jerremyng
53d4a8399f feat(#152): preview polling url
add error handling for preview

fix idx bug and add tests

fix light mode styling and remove transitions

add global styling class
2026-01-10 17:44:51 +01:00
11 changed files with 274 additions and 187 deletions

2
.gitignore vendored
View file

@ -32,3 +32,5 @@ yarn-error.log
/.ai /.ai
.DS_Store .DS_Store
/boost.json /boost.json
/.gemini
/GEMINI.md

View file

@ -153,12 +153,13 @@ class Plugin extends Model
public function updateDataPayload(): void public function updateDataPayload(): void
{ {
if ($this->data_strategy === 'polling' && $this->polling_url) { if ($this->data_strategy !== 'polling' || !$this->polling_url) {
return;
}
$headers = ['User-Agent' => 'usetrmnl/byos_laravel', 'Accept' => 'application/json']; $headers = ['User-Agent' => 'usetrmnl/byos_laravel', 'Accept' => 'application/json'];
// resolve headers
if ($this->polling_header) { if ($this->polling_header) {
// Resolve Liquid variables in the polling header
$resolvedHeader = $this->resolveLiquidVariables($this->polling_header); $resolvedHeader = $this->resolveLiquidVariables($this->polling_header);
$headerLines = explode("\n", mb_trim($resolvedHeader)); $headerLines = explode("\n", mb_trim($resolvedHeader));
foreach ($headerLines as $line) { foreach ($headerLines as $line) {
@ -169,90 +170,51 @@ class Plugin extends Model
} }
} }
// Resolve Liquid variables in the entire polling_url field first, then split by newline // resolve and clean URLs
$resolvedPollingUrls = $this->resolveLiquidVariables($this->polling_url); $resolvedPollingUrls = $this->resolveLiquidVariables($this->polling_url);
$urls = array_filter( $urls = array_values(array_filter( // array_values ensures 0, 1, 2...
array_map('trim', explode("\n", $resolvedPollingUrls)), array_map('trim', explode("\n", $resolvedPollingUrls)),
fn ($url): bool => ! empty($url) fn ($url): bool => filled($url)
); ));
// If only one URL, use the original logic without nesting
if (count($urls) === 1) {
$url = reset($urls);
$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);
$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 = []; $combinedResponse = [];
// Loop through all URLs (Handles 1 or many)
foreach ($urls as $index => $url) { foreach ($urls as $index => $url) {
$httpRequest = Http::withHeaders($headers); $httpRequest = Http::withHeaders($headers);
if ($this->polling_verb === 'post' && $this->polling_body) { if ($this->polling_verb === 'post' && $this->polling_body) {
// Resolve Liquid variables in the polling body
$resolvedBody = $this->resolveLiquidVariables($this->polling_body); $resolvedBody = $this->resolveLiquidVariables($this->polling_body);
$httpRequest = $httpRequest->withBody($resolvedBody); $httpRequest = $httpRequest->withBody($resolvedBody);
} }
// URL is already resolved, use it directly
$resolvedUrl = $url;
try { try {
// Make the request based on the verb $httpResponse = ($this->polling_verb === 'post')
$httpResponse = $this->polling_verb === 'post' ? $httpRequest->post($resolvedUrl) : $httpRequest->get($resolvedUrl); ? $httpRequest->post($url)
: $httpRequest->get($url);
$response = $this->parseResponse($httpResponse); $response = $this->parseResponse($httpResponse);
// Check if response is an array at root level // Nest if it's a sequential array
if (array_keys($response) === range(0, count($response) - 1)) { if (array_keys($response) === range(0, count($response) - 1)) {
// Response is a sequential array, nest under .data
$combinedResponse["IDX_{$index}"] = ['data' => $response]; $combinedResponse["IDX_{$index}"] = ['data' => $response];
} else { } else {
// Response is an object or associative array, keep as is
$combinedResponse["IDX_{$index}"] = $response; $combinedResponse["IDX_{$index}"] = $response;
} }
} catch (Exception $e) { } catch (Exception $e) {
// Log error and continue with other URLs Log::warning("Failed to fetch data from URL {$url}: ".$e->getMessage());
Log::warning("Failed to fetch data from URL {$resolvedUrl}: ".$e->getMessage());
$combinedResponse["IDX_{$index}"] = ['error' => 'Failed to fetch data']; $combinedResponse["IDX_{$index}"] = ['error' => 'Failed to fetch data'];
} }
} }
// unwrap IDX_0 if only one URL
$finalPayload = (count($urls) === 1) ? reset($combinedResponse) : $combinedResponse;
$this->update([ $this->update([
'data_payload' => $combinedResponse, 'data_payload' => $finalPayload,
'data_payload_updated_at' => now(), 'data_payload_updated_at' => now(),
]); ]);
} }
}
private function parseResponse(Response $httpResponse): array private function parseResponse(Response $httpResponse): array
{ {

146
composer.lock generated
View file

@ -62,16 +62,16 @@
}, },
{ {
"name": "aws/aws-sdk-php", "name": "aws/aws-sdk-php",
"version": "3.369.6", "version": "3.369.10",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/aws/aws-sdk-php.git", "url": "https://github.com/aws/aws-sdk-php.git",
"reference": "b1e1846a4b6593b6916764d86fc0890a31727370" "reference": "e179090bf2d658be7be37afc146111966ba6f41b"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/b1e1846a4b6593b6916764d86fc0890a31727370", "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/e179090bf2d658be7be37afc146111966ba6f41b",
"reference": "b1e1846a4b6593b6916764d86fc0890a31727370", "reference": "e179090bf2d658be7be37afc146111966ba6f41b",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -153,9 +153,9 @@
"support": { "support": {
"forum": "https://github.com/aws/aws-sdk-php/discussions", "forum": "https://github.com/aws/aws-sdk-php/discussions",
"issues": "https://github.com/aws/aws-sdk-php/issues", "issues": "https://github.com/aws/aws-sdk-php/issues",
"source": "https://github.com/aws/aws-sdk-php/tree/3.369.6" "source": "https://github.com/aws/aws-sdk-php/tree/3.369.10"
}, },
"time": "2026-01-02T19:09:23+00:00" "time": "2026-01-09T19:08:12+00:00"
}, },
{ {
"name": "bnussbau/laravel-trmnl-blade", "name": "bnussbau/laravel-trmnl-blade",
@ -877,16 +877,16 @@
}, },
{ {
"name": "firebase/php-jwt", "name": "firebase/php-jwt",
"version": "v6.11.1", "version": "v7.0.2",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/firebase/php-jwt.git", "url": "https://github.com/firebase/php-jwt.git",
"reference": "d1e91ecf8c598d073d0995afa8cd5c75c6e19e66" "reference": "5645b43af647b6947daac1d0f659dd1fbe8d3b65"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/firebase/php-jwt/zipball/d1e91ecf8c598d073d0995afa8cd5c75c6e19e66", "url": "https://api.github.com/repos/firebase/php-jwt/zipball/5645b43af647b6947daac1d0f659dd1fbe8d3b65",
"reference": "d1e91ecf8c598d073d0995afa8cd5c75c6e19e66", "reference": "5645b43af647b6947daac1d0f659dd1fbe8d3b65",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -934,9 +934,9 @@
], ],
"support": { "support": {
"issues": "https://github.com/firebase/php-jwt/issues", "issues": "https://github.com/firebase/php-jwt/issues",
"source": "https://github.com/firebase/php-jwt/tree/v6.11.1" "source": "https://github.com/firebase/php-jwt/tree/v7.0.2"
}, },
"time": "2025-04-09T20:32:01+00:00" "time": "2025-12-16T22:17:28+00:00"
}, },
{ {
"name": "fruitcake/php-cors", "name": "fruitcake/php-cors",
@ -1678,16 +1678,16 @@
}, },
{ {
"name": "laravel/framework", "name": "laravel/framework",
"version": "v12.44.0", "version": "v12.46.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/laravel/framework.git", "url": "https://github.com/laravel/framework.git",
"reference": "592bbf1c036042958332eb98e3e8131b29102f33" "reference": "9dcff48d25a632c1fadb713024c952fec489c4ae"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/laravel/framework/zipball/592bbf1c036042958332eb98e3e8131b29102f33", "url": "https://api.github.com/repos/laravel/framework/zipball/9dcff48d25a632c1fadb713024c952fec489c4ae",
"reference": "592bbf1c036042958332eb98e3e8131b29102f33", "reference": "9dcff48d25a632c1fadb713024c952fec489c4ae",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -1896,7 +1896,7 @@
"issues": "https://github.com/laravel/framework/issues", "issues": "https://github.com/laravel/framework/issues",
"source": "https://github.com/laravel/framework" "source": "https://github.com/laravel/framework"
}, },
"time": "2025-12-23T15:29:43+00:00" "time": "2026-01-07T23:26:53+00:00"
}, },
{ {
"name": "laravel/prompts", "name": "laravel/prompts",
@ -1959,16 +1959,16 @@
}, },
{ {
"name": "laravel/sanctum", "name": "laravel/sanctum",
"version": "v4.2.1", "version": "v4.2.2",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/laravel/sanctum.git", "url": "https://github.com/laravel/sanctum.git",
"reference": "f5fb373be39a246c74a060f2cf2ae2c2145b3664" "reference": "fd447754d2d3f56950d53b930128af2e3b617de9"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/laravel/sanctum/zipball/f5fb373be39a246c74a060f2cf2ae2c2145b3664", "url": "https://api.github.com/repos/laravel/sanctum/zipball/fd447754d2d3f56950d53b930128af2e3b617de9",
"reference": "f5fb373be39a246c74a060f2cf2ae2c2145b3664", "reference": "fd447754d2d3f56950d53b930128af2e3b617de9",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -2018,7 +2018,7 @@
"issues": "https://github.com/laravel/sanctum/issues", "issues": "https://github.com/laravel/sanctum/issues",
"source": "https://github.com/laravel/sanctum" "source": "https://github.com/laravel/sanctum"
}, },
"time": "2025-11-21T13:59:03+00:00" "time": "2026-01-06T23:11:51+00:00"
}, },
{ {
"name": "laravel/serializable-closure", "name": "laravel/serializable-closure",
@ -2083,21 +2083,21 @@
}, },
{ {
"name": "laravel/socialite", "name": "laravel/socialite",
"version": "v5.24.0", "version": "v5.24.1",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/laravel/socialite.git", "url": "https://github.com/laravel/socialite.git",
"reference": "1d19358c28e8951dde6e36603b89d8f09e6cfbfd" "reference": "25e28c14d55404886777af1d77cf030e0f633142"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/laravel/socialite/zipball/1d19358c28e8951dde6e36603b89d8f09e6cfbfd", "url": "https://api.github.com/repos/laravel/socialite/zipball/25e28c14d55404886777af1d77cf030e0f633142",
"reference": "1d19358c28e8951dde6e36603b89d8f09e6cfbfd", "reference": "25e28c14d55404886777af1d77cf030e0f633142",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
"ext-json": "*", "ext-json": "*",
"firebase/php-jwt": "^6.4", "firebase/php-jwt": "^6.4|^7.0",
"guzzlehttp/guzzle": "^6.0|^7.0", "guzzlehttp/guzzle": "^6.0|^7.0",
"illuminate/contracts": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.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", "illuminate/http": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0",
@ -2151,20 +2151,20 @@
"issues": "https://github.com/laravel/socialite/issues", "issues": "https://github.com/laravel/socialite/issues",
"source": "https://github.com/laravel/socialite" "source": "https://github.com/laravel/socialite"
}, },
"time": "2025-12-09T15:37:06+00:00" "time": "2026-01-01T02:57:21+00:00"
}, },
{ {
"name": "laravel/tinker", "name": "laravel/tinker",
"version": "v2.10.2", "version": "v2.11.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/laravel/tinker.git", "url": "https://github.com/laravel/tinker.git",
"reference": "3bcb5f62d6f837e0f093a601e26badafb127bd4c" "reference": "3d34b97c9a1747a81a3fde90482c092bd8b66468"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/laravel/tinker/zipball/3bcb5f62d6f837e0f093a601e26badafb127bd4c", "url": "https://api.github.com/repos/laravel/tinker/zipball/3d34b97c9a1747a81a3fde90482c092bd8b66468",
"reference": "3bcb5f62d6f837e0f093a601e26badafb127bd4c", "reference": "3d34b97c9a1747a81a3fde90482c092bd8b66468",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -2173,7 +2173,7 @@
"illuminate/support": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0", "illuminate/support": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0",
"php": "^7.2.5|^8.0", "php": "^7.2.5|^8.0",
"psy/psysh": "^0.11.1|^0.12.0", "psy/psysh": "^0.11.1|^0.12.0",
"symfony/var-dumper": "^4.3.4|^5.0|^6.0|^7.0" "symfony/var-dumper": "^4.3.4|^5.0|^6.0|^7.0|^8.0"
}, },
"require-dev": { "require-dev": {
"mockery/mockery": "~1.3.3|^1.4.2", "mockery/mockery": "~1.3.3|^1.4.2",
@ -2215,9 +2215,9 @@
], ],
"support": { "support": {
"issues": "https://github.com/laravel/tinker/issues", "issues": "https://github.com/laravel/tinker/issues",
"source": "https://github.com/laravel/tinker/tree/v2.10.2" "source": "https://github.com/laravel/tinker/tree/v2.11.0"
}, },
"time": "2025-11-20T16:29:12+00:00" "time": "2025-12-19T19:16:45+00:00"
}, },
{ {
"name": "league/commonmark", "name": "league/commonmark",
@ -8096,16 +8096,16 @@
"packages-dev": [ "packages-dev": [
{ {
"name": "brianium/paratest", "name": "brianium/paratest",
"version": "v7.16.0", "version": "v7.16.1",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/paratestphp/paratest.git", "url": "https://github.com/paratestphp/paratest.git",
"reference": "a10878ed0fe0bbc2f57c980f7a08065338b970b6" "reference": "f0fdfd8e654e0d38bc2ba756a6cabe7be287390b"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/paratestphp/paratest/zipball/a10878ed0fe0bbc2f57c980f7a08065338b970b6", "url": "https://api.github.com/repos/paratestphp/paratest/zipball/f0fdfd8e654e0d38bc2ba756a6cabe7be287390b",
"reference": "a10878ed0fe0bbc2f57c980f7a08065338b970b6", "reference": "f0fdfd8e654e0d38bc2ba756a6cabe7be287390b",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -8116,10 +8116,10 @@
"fidry/cpu-core-counter": "^1.3.0", "fidry/cpu-core-counter": "^1.3.0",
"jean85/pretty-package-versions": "^2.1.1", "jean85/pretty-package-versions": "^2.1.1",
"php": "~8.3.0 || ~8.4.0 || ~8.5.0", "php": "~8.3.0 || ~8.4.0 || ~8.5.0",
"phpunit/php-code-coverage": "^12.5.1", "phpunit/php-code-coverage": "^12.5.2",
"phpunit/php-file-iterator": "^6", "phpunit/php-file-iterator": "^6",
"phpunit/php-timer": "^8", "phpunit/php-timer": "^8",
"phpunit/phpunit": "^12.5.2", "phpunit/phpunit": "^12.5.4",
"sebastian/environment": "^8.0.3", "sebastian/environment": "^8.0.3",
"symfony/console": "^7.3.4 || ^8.0.0", "symfony/console": "^7.3.4 || ^8.0.0",
"symfony/process": "^7.3.4 || ^8.0.0" "symfony/process": "^7.3.4 || ^8.0.0"
@ -8131,7 +8131,7 @@
"ext-posix": "*", "ext-posix": "*",
"phpstan/phpstan": "^2.1.33", "phpstan/phpstan": "^2.1.33",
"phpstan/phpstan-deprecation-rules": "^2.0.3", "phpstan/phpstan-deprecation-rules": "^2.0.3",
"phpstan/phpstan-phpunit": "^2.0.10", "phpstan/phpstan-phpunit": "^2.0.11",
"phpstan/phpstan-strict-rules": "^2.0.7", "phpstan/phpstan-strict-rules": "^2.0.7",
"symfony/filesystem": "^7.3.2 || ^8.0.0" "symfony/filesystem": "^7.3.2 || ^8.0.0"
}, },
@ -8173,7 +8173,7 @@
], ],
"support": { "support": {
"issues": "https://github.com/paratestphp/paratest/issues", "issues": "https://github.com/paratestphp/paratest/issues",
"source": "https://github.com/paratestphp/paratest/tree/v7.16.0" "source": "https://github.com/paratestphp/paratest/tree/v7.16.1"
}, },
"funding": [ "funding": [
{ {
@ -8185,7 +8185,7 @@
"type": "paypal" "type": "paypal"
} }
], ],
"time": "2025-12-09T20:03:26+00:00" "time": "2026-01-08T07:23:06+00:00"
}, },
{ {
"name": "doctrine/deprecations", "name": "doctrine/deprecations",
@ -8674,16 +8674,16 @@
}, },
{ {
"name": "laravel/boost", "name": "laravel/boost",
"version": "v1.8.7", "version": "v1.8.9",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/laravel/boost.git", "url": "https://github.com/laravel/boost.git",
"reference": "7a5709a8134ed59d3e7f34fccbd74689830e296c" "reference": "1f2c2d41b5216618170fb6730ec13bf894c5bffd"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/laravel/boost/zipball/7a5709a8134ed59d3e7f34fccbd74689830e296c", "url": "https://api.github.com/repos/laravel/boost/zipball/1f2c2d41b5216618170fb6730ec13bf894c5bffd",
"reference": "7a5709a8134ed59d3e7f34fccbd74689830e296c", "reference": "1f2c2d41b5216618170fb6730ec13bf894c5bffd",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -8736,20 +8736,20 @@
"issues": "https://github.com/laravel/boost/issues", "issues": "https://github.com/laravel/boost/issues",
"source": "https://github.com/laravel/boost" "source": "https://github.com/laravel/boost"
}, },
"time": "2025-12-19T15:04:12+00:00" "time": "2026-01-07T18:43:11+00:00"
}, },
{ {
"name": "laravel/mcp", "name": "laravel/mcp",
"version": "v0.5.1", "version": "v0.5.2",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/laravel/mcp.git", "url": "https://github.com/laravel/mcp.git",
"reference": "10dedea054fa4eeaa9ef2ccbfdad6c3e1dbd17a4" "reference": "b9bdd8d6f8b547c8733fe6826b1819341597ba3c"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/laravel/mcp/zipball/10dedea054fa4eeaa9ef2ccbfdad6c3e1dbd17a4", "url": "https://api.github.com/repos/laravel/mcp/zipball/b9bdd8d6f8b547c8733fe6826b1819341597ba3c",
"reference": "10dedea054fa4eeaa9ef2ccbfdad6c3e1dbd17a4", "reference": "b9bdd8d6f8b547c8733fe6826b1819341597ba3c",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -8809,7 +8809,7 @@
"issues": "https://github.com/laravel/mcp/issues", "issues": "https://github.com/laravel/mcp/issues",
"source": "https://github.com/laravel/mcp" "source": "https://github.com/laravel/mcp"
}, },
"time": "2025-12-17T06:14:23+00:00" "time": "2025-12-19T19:32:34+00:00"
}, },
{ {
"name": "laravel/pail", "name": "laravel/pail",
@ -8892,16 +8892,16 @@
}, },
{ {
"name": "laravel/pint", "name": "laravel/pint",
"version": "v1.26.0", "version": "v1.27.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/laravel/pint.git", "url": "https://github.com/laravel/pint.git",
"reference": "69dcca060ecb15e4b564af63d1f642c81a241d6f" "reference": "c67b4195b75491e4dfc6b00b1c78b68d86f54c90"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/laravel/pint/zipball/69dcca060ecb15e4b564af63d1f642c81a241d6f", "url": "https://api.github.com/repos/laravel/pint/zipball/c67b4195b75491e4dfc6b00b1c78b68d86f54c90",
"reference": "69dcca060ecb15e4b564af63d1f642c81a241d6f", "reference": "c67b4195b75491e4dfc6b00b1c78b68d86f54c90",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -8912,9 +8912,9 @@
"php": "^8.2.0" "php": "^8.2.0"
}, },
"require-dev": { "require-dev": {
"friendsofphp/php-cs-fixer": "^3.90.0", "friendsofphp/php-cs-fixer": "^3.92.4",
"illuminate/view": "^12.40.1", "illuminate/view": "^12.44.0",
"larastan/larastan": "^3.8.0", "larastan/larastan": "^3.8.1",
"laravel-zero/framework": "^12.0.4", "laravel-zero/framework": "^12.0.4",
"mockery/mockery": "^1.6.12", "mockery/mockery": "^1.6.12",
"nunomaduro/termwind": "^2.3.3", "nunomaduro/termwind": "^2.3.3",
@ -8955,7 +8955,7 @@
"issues": "https://github.com/laravel/pint/issues", "issues": "https://github.com/laravel/pint/issues",
"source": "https://github.com/laravel/pint" "source": "https://github.com/laravel/pint"
}, },
"time": "2025-11-25T21:15:52+00:00" "time": "2026-01-05T16:49:17+00:00"
}, },
{ {
"name": "laravel/roster", "name": "laravel/roster",
@ -9020,16 +9020,16 @@
}, },
{ {
"name": "laravel/sail", "name": "laravel/sail",
"version": "v1.51.0", "version": "v1.52.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/laravel/sail.git", "url": "https://github.com/laravel/sail.git",
"reference": "1c74357df034e869250b4365dd445c9f6ba5d068" "reference": "64ac7d8abb2dbcf2b76e61289451bae79066b0b3"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/laravel/sail/zipball/1c74357df034e869250b4365dd445c9f6ba5d068", "url": "https://api.github.com/repos/laravel/sail/zipball/64ac7d8abb2dbcf2b76e61289451bae79066b0b3",
"reference": "1c74357df034e869250b4365dd445c9f6ba5d068", "reference": "64ac7d8abb2dbcf2b76e61289451bae79066b0b3",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -9079,7 +9079,7 @@
"issues": "https://github.com/laravel/sail/issues", "issues": "https://github.com/laravel/sail/issues",
"source": "https://github.com/laravel/sail" "source": "https://github.com/laravel/sail"
}, },
"time": "2025-12-09T13:33:49+00:00" "time": "2026-01-01T02:46:03+00:00"
}, },
{ {
"name": "mockery/mockery", "name": "mockery/mockery",
@ -11806,16 +11806,16 @@
}, },
{ {
"name": "webmozart/assert", "name": "webmozart/assert",
"version": "2.0.0", "version": "2.1.1",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/webmozarts/assert.git", "url": "https://github.com/webmozarts/assert.git",
"reference": "1b34b004e35a164bc5bb6ebd33c844b2d8069a54" "reference": "bdbabc199a7ba9965484e4725d66170e5711323b"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/webmozarts/assert/zipball/1b34b004e35a164bc5bb6ebd33c844b2d8069a54", "url": "https://api.github.com/repos/webmozarts/assert/zipball/bdbabc199a7ba9965484e4725d66170e5711323b",
"reference": "1b34b004e35a164bc5bb6ebd33c844b2d8069a54", "reference": "bdbabc199a7ba9965484e4725d66170e5711323b",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -11862,9 +11862,9 @@
], ],
"support": { "support": {
"issues": "https://github.com/webmozarts/assert/issues", "issues": "https://github.com/webmozarts/assert/issues",
"source": "https://github.com/webmozarts/assert/tree/2.0.0" "source": "https://github.com/webmozarts/assert/tree/2.1.1"
}, },
"time": "2025-12-16T21:36:00+00:00" "time": "2026-01-08T11:28:40+00:00"
} }
], ],
"aliases": [], "aliases": [],

View file

@ -72,3 +72,39 @@ select:focus[data-flux-control] {
/* \[:where(&)\]:size-4 { /* \[:where(&)\]:size-4 {
@apply 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;
}
}

View file

@ -15,7 +15,7 @@
</a> </a>
<div class="flex flex-col gap-6"> <div class="flex flex-col gap-6">
<div class="rounded-xl border bg-white dark:bg-stone-950 dark:border-stone-800 text-stone-800 shadow-xs"> <div class="styled-container">
<div class="px-10 py-8">{{ $slot }}</div> <div class="px-10 py-8">{{ $slot }}</div>
</div> </div>
</div> </div>

View file

@ -16,7 +16,7 @@ new class extends Component {
@if($devices->isEmpty()) @if($devices->isEmpty())
<div class="flex flex-col gap-6"> <div class="flex flex-col gap-6">
<div <div
class="rounded-xl border bg-white dark:bg-stone-950 dark:border-stone-800 text-stone-800 shadow-xs"> class="styled-container">
<div class="px-10 py-8"> <div class="px-10 py-8">
<h1 class="text-xl font-medium dark:text-zinc-200">Add your first device</h1> <h1 class="text-xl font-medium dark:text-zinc-200">Add your first device</h1>
<flux:button href="{{ route('devices') }}" class="mt-4" icon="plus-circle" variant="primary" <flux:button href="{{ route('devices') }}" class="mt-4" icon="plus-circle" variant="primary"
@ -30,7 +30,7 @@ new class extends Component {
@foreach($devices as $device) @foreach($devices as $device)
<div class="flex flex-col gap-6"> <div class="flex flex-col gap-6">
<div <div
class="rounded-xl border bg-white dark:bg-stone-950 dark:border-stone-800 text-stone-800 shadow-xs"> class="styled-container">
<div class="px-10 py-8"> <div class="px-10 py-8">
@php @php
$current_image_uuid =$device->current_screen_image; $current_image_uuid =$device->current_screen_image;

View file

@ -309,7 +309,7 @@ new class extends Component {
<div class="bg-muted flex flex-col items-center justify-center gap-6 p-6 md:p-10"> <div class="bg-muted flex flex-col items-center justify-center gap-6 p-6 md:p-10">
<div class="flex flex-col gap-6"> <div class="flex flex-col gap-6">
<div <div
class="rounded-xl border bg-white dark:bg-stone-950 dark:border-stone-800 text-stone-800 shadow-xs"> class="styled-container">
<div class="px-10 py-8"> <div class="px-10 py-8">
@php @php
$current_image_uuid =$device->current_screen_image; $current_image_uuid =$device->current_screen_image;

View file

@ -332,7 +332,7 @@ new class extends Component {
@endforeach @endforeach
@if($devices->isEmpty() || $devices->every(fn($device) => $device->playlists->isEmpty())) @if($devices->isEmpty() || $devices->every(fn($device) => $device->playlists->isEmpty()))
<div class="rounded-xl border bg-white dark:bg-stone-950 dark:border-stone-800 text-stone-800 shadow-xs"> <div class="styled-container">
<div class="px-10 py-8"> <div class="px-10 py-8">
<h1 class="text-xl font-medium dark:text-zinc-200">No playlists found</h1> <h1 class="text-xl font-medium dark:text-zinc-200">No playlists found</h1>
<p class="text-sm dark:text-zinc-400 mt-2">Add playlists to your devices to see them here.</p> <p class="text-sm dark:text-zinc-400 mt-2">Add playlists to your devices to see them here.</p>

View file

@ -395,7 +395,7 @@ new class extends Component {
wire:key="plugin-{{ $plugin['id'] ?? $plugin['name'] ?? $index }}" wire:key="plugin-{{ $plugin['id'] ?? $plugin['name'] ?? $index }}"
x-data="{ pluginName: {{ json_encode(strtolower($plugin['name'] ?? '')) }} }" x-data="{ pluginName: {{ json_encode(strtolower($plugin['name'] ?? '')) }} }"
x-show="searchTerm.length <= 1 || pluginName.includes(searchTerm.toLowerCase())" x-show="searchTerm.length <= 1 || pluginName.includes(searchTerm.toLowerCase())"
class="rounded-xl border bg-white dark:bg-stone-950 dark:border-stone-800 text-stone-800 shadow-xs"> class="styled-container">
<a href="{{ ($plugin['detail_view_route']) ? route($plugin['detail_view_route']) : route('plugins.recipe', ['plugin' => $plugin['id']]) }}" <a href="{{ ($plugin['detail_view_route']) ? route($plugin['detail_view_route']) : route('plugins.recipe', ['plugin' => $plugin['id']]) }}"
class="block h-full"> class="block h-full">
<div class="flex items-center space-x-4 px-10 py-8 h-full"> <div class="flex items-center space-x-4 px-10 py-8 h-full">

View file

@ -10,6 +10,7 @@ use Illuminate\Support\Facades\Blade;
use Illuminate\Support\Arr; use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Http;
use Livewire\Attributes\On; use Livewire\Attributes\On;
use Livewire\Attributes\Computed;
new class extends Component { new class extends Component {
public Plugin $plugin; public Plugin $plugin;
@ -295,8 +296,6 @@ new class extends Component {
return $this->configuration[$key] ?? $default; return $this->configuration[$key] ?? $default;
} }
public function renderExample(string $example) public function renderExample(string $example)
{ {
switch ($example) { switch ($example) {
@ -431,6 +430,21 @@ HTML;
$this->plugin = $this->plugin->fresh(); $this->plugin = $this->plugin->fresh();
} }
// Laravel Livewire computed property: access with $this->parsed_urls
#[Computed]
private function parsedUrls()
{
if (!isset($this->polling_url)) {
return null;
}
try {
return $this->plugin->resolveLiquidVariables($this->polling_url);
} catch (\Exception $e) {
return 'PARSE_ERROR: ' . $e->getMessage();
}
}
} }
?> ?>
@ -733,15 +747,62 @@ HTML;
</div> </div>
@if($data_strategy === 'polling') @if($data_strategy === 'polling')
<div class="mb-4"> <flux:label>Polling URL</flux:label>
<flux:textarea label="Polling URL" description="You can use configuration variables with Liquid syntax. Supports multiple requests via line break separation" wire:model="polling_url" id="polling_url"
<div x-data="{ subTab: 'settings' }" class="mt-2 mb-4">
<div class="flex">
<button
@click="subTab = 'settings'"
class="tab-button"
:class="subTab === 'settings' ? 'is-active' : ''"
>
<flux:icon.cog-6-tooth class="size-4"/>
Settings
</button>
<button
@click="subTab = 'preview'"
class="tab-button"
:class="subTab === 'preview' ? 'is-active' : ''"
>
<flux:icon.eye class="size-4" />
Preview URL
</button>
</div>
<div class="flex-col p-4 bg-transparent rounded-tl-none styled-container">
<div x-show="subTab === 'settings'">
<flux:field>
<flux:description>Enter the URL(s) to poll for data:</flux:description>
<flux:textarea
wire:model.live="polling_url"
placeholder="https://example.com/api" placeholder="https://example.com/api"
class="block w-full" type="text" name="polling_url" autofocus> rows="5"
</flux:input> />
<flux:button icon="cloud-arrow-down" wire:click="updateData" class="block mt-2 w-full"> <flux:description>
{!! 'Hint: Supports multiple requests via line break separation. You can also use configuration variables with <a href="https://help.usetrmnl.com/en/articles/12689499-dynamic-polling-urls">Liquid syntax</a>. ' !!}
</flux:description>
</flux:field>
</div>
<div x-show="subTab === 'preview'" x-cloak>
<flux:field>
<flux:description>Preview computed URLs here (readonly):</flux:description>
<flux:textarea
readonly
placeholder="Nothing to show..."
rows="5"
>
{{ $this->parsed_urls }}
</flux:textarea>
</flux:field>
</div>
<flux:button variant="primary" icon="cloud-arrow-down" wire:click="updateData" class="w-full mt-4">
Fetch data now Fetch data now
</flux:button> </flux:button>
</div> </div>
</div>
<div class="mb-4"> <div class="mb-4">
<flux:radio.group wire:model.live="polling_verb" label="Polling Verb" variant="segmented"> <flux:radio.group wire:model.live="polling_verb" label="Polling Verb" variant="segmented">
@ -905,9 +966,6 @@ HTML;
</div> </div>
</flux:field> </flux:field>
</div> </div>
@else @else
<div class="flex items-center gap-6 mb-4 mt-4"> <div class="flex items-center gap-6 mb-4 mt-4">

View file

@ -99,6 +99,35 @@ test('updateDataPayload handles multiple URLs with IDX_ prefixes', function ():
expect($plugin->data_payload['IDX_2'])->toBe(['headline' => 'test']); 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 { test('updateDataPayload handles single URL without nesting', function (): void {
$plugin = Plugin::factory()->create([ $plugin = Plugin::factory()->create([
'data_strategy' => 'polling', 'data_strategy' => 'polling',