add features

* feat: autojoin toggle
* feat: auto add devices
* feat: proxy feature
* feat: support puppeteer in docker
* feat: toggle to activate cloud proxy
* feat: relay device information
* feat: relay logs to cloud
* feat: migrate on start
* feat: calculate battery state, wifi signal
* feat: eye candy for configure view
* feat: update via api
This commit is contained in:
Benjamin Nussbaum 2025-02-26 09:33:54 +01:00
parent d4eb832186
commit 715e6a2562
53 changed files with 1459 additions and 460 deletions

24
.dockerignore Normal file
View file

@ -0,0 +1,24 @@
/.phpunit.cache
/node_modules
/public/build
/public/hot
/public/storage
/storage/*.key
/storage/pail
/vendor
.env
.env.backup
.env.production
.phpactor.json
.phpunit.result.cache
Homestead.json
Homestead.yaml
auth.json
npm-debug.log
yarn-error.log
/.fleet
/.idea
/.vscode
/.zed
/bootstrap/cache/*
/database/database.sqlite

View file

@ -1,7 +1,7 @@
APP_NAME=Laravel APP_NAME=TrmnlServer
APP_ENV=local APP_ENV=production
APP_KEY= APP_KEY=base64:zzPXBQPlgn0NHwVBTVG0B//8P/PVwVnBp2gk0ZWR0+k=
APP_DEBUG=true APP_DEBUG=false
APP_TIMEZONE=UTC APP_TIMEZONE=UTC
APP_URL=http://localhost APP_URL=http://localhost
@ -17,7 +17,7 @@ PHP_CLI_SERVER_WORKERS=4
BCRYPT_ROUNDS=12 BCRYPT_ROUNDS=12
LOG_CHANNEL=stack LOG_CHANNEL=stack
LOG_STACK=single LOG_STACK=single,stderr
LOG_DEPRECATIONS_CHANNEL=null LOG_DEPRECATIONS_CHANNEL=null
LOG_LEVEL=debug LOG_LEVEL=debug
@ -64,3 +64,7 @@ AWS_BUCKET=
AWS_USE_PATH_STYLE_ENDPOINT=false AWS_USE_PATH_STYLE_ENDPOINT=false
VITE_APP_NAME="${APP_NAME}" VITE_APP_NAME="${APP_NAME}"
TRMNL_PROXY_BASE_URL=https://trmnl.app
TRMNL_PROXY_REFRESH_MINUTES=15
REGISTRATION_ENABLED=1

68
Dockerfile Normal file
View file

@ -0,0 +1,68 @@
FROM php:8.3-fpm-alpine3.20
# Install system dependencies
RUN apk add --no-cache \
nginx \
supervisor \
libpq \
nodejs \
npm \
git \
curl \
zip \
unzip \
imagemagick-dev \
chromium
# Configure Chromium Path
ENV PUPPETEER_EXECUTABLE_PATH /usr/bin/chromium
ENV PUPPETEER_DOCKER 1
#RUN apk add --no-cache --virtual .build-deps $PHPIZE_DEPS imagemagick-dev \
#&& pecl install imagick \
#&& docker-php-ext-enable imagick \
#&& apk del .build-deps \
#RUN docker-php-ext-install imagick \
# && docker-php-ext-enable imagick
RUN mkdir -p /usr/src/php/ext/imagick
RUN chmod 777 /usr/src/php/ext/imagick
RUN curl -fsSL https://github.com/Imagick/imagick/archive/refs/tags/3.7.0.tar.gz | tar xvz -C "/usr/src/php/ext/imagick" --strip 1
# Install PHP extensions
RUN docker-php-ext-install opcache imagick
# Install composer
COPY --from=composer:latest /usr/bin/composer /usr/bin/composer
# Set working directory
WORKDIR /var/www/html
# Copy application files
COPY --chown=www-data:www-data . .
COPY --chown=www-data:www-data ./.env.example ./.env
# Install application dependencies
RUN composer install --no-dev --optimize-autoloader --no-interaction
RUN npm install && npm run build
# Copy configuration files
COPY docker/nginx.conf /etc/nginx/http.d/default.conf
COPY docker/supervisord.conf /etc/supervisor/conf.d/supervisord.conf
COPY docker/php.ini /usr/local/etc/php/conf.d/custom.ini
# Create required directories
RUN mkdir -p /var/log/supervisor \
&& mkdir -p storage/logs \
&& mkdir -p storage/framework/{cache,sessions,views} \
&& chmod -R 775 storage \
&& chmod -R 775 bootstrap/cache \
&& touch database/database.sqlite \
&& chmod -R 777 database
# Expose port 80
EXPOSE 80
# Start supervisor
CMD ["/usr/bin/supervisord", "-n", "-c", "/etc/supervisor/conf.d/supervisord.conf"]

View file

@ -0,0 +1,18 @@
<?php
namespace App\Console\Commands;
use App\Jobs\FetchProxyCloudResponses;
use Illuminate\Console\Command;
class FetchProxyCloudResponsesCommand extends Command
{
protected $signature = 'trmnl:cloud:proxy';
protected $description = 'Fetch Cloud Screens';
public function handle(): void
{
FetchProxyCloudResponses::dispatchSync();
}
}

View file

@ -2,11 +2,8 @@
namespace App\Console\Commands; namespace App\Console\Commands;
use App\Models\Device; use App\Jobs\GenerateScreenJob;
use Illuminate\Console\Command; use Illuminate\Console\Command;
use Illuminate\Support\Facades\Storage;
use Ramsey\Uuid\Uuid;
use Spatie\Browsershot\Browsershot;
class ScreenGeneratorCommand extends Command class ScreenGeneratorCommand extends Command
{ {
@ -22,101 +19,28 @@ class ScreenGeneratorCommand extends Command
* *
* @var string * @var string
*/ */
protected $description = ''; protected $description = 'Generate a screen for a terminal device';
/** /**
* Execute the console command. * Execute the console command.
*/ */
public function handle() public function handle()
{ {
$deviceId = $this->argument('deviceId'); $deviceId = $this->argument('deviceId');
$view = $this->argument('view'); $view = $this->argument('view');
$uuid = Uuid::uuid4()->toString();
$pngPath = Storage::disk('public')->path('/images/generated/'.$uuid.'.png');
$bmpPath = Storage::disk('public')->path('/images/generated/'.$uuid.'.bmp');
// Generate PNG
try { try {
Browsershot::html(view($view)->render()) $markup = view($view)->render();
->windowSize(800, 480) } catch (\Throwable $e) {
->save($pngPath); $this->error('Failed to render view: '.$e->getMessage());
} catch (\Exception $e) {
$this->error('Failed to generate PNG: '.$e->getMessage());
return; return 1;
} }
try { GenerateScreenJob::dispatchSync($deviceId, $markup);
$this->convertToBmpImageMagick($pngPath, $bmpPath);
} catch (\ImagickException $e) { $this->info('Screen generation job finished.');
$this->error('Failed to convert image to BMP: '.$e->getMessage());
}
Device::find($deviceId)->update(['current_screen_image' => $uuid]); return 0;
$this->cleanupFolder();
}
/**
* @throws \ImagickException
*/
public function convertToBmpImageMagick(string $pngPath, string $bmpPath): void
{
$imagick = new \Imagick($pngPath);
$imagick->setImageType(\Imagick::IMGTYPE_GRAYSCALE);
$imagick->quantizeImage(2, \Imagick::COLORSPACE_GRAY, 0, true, false);
$imagick->setImageDepth(1);
$imagick->stripImage();
$imagick->setFormat('BMP3');
$imagick->writeImage($bmpPath);
$imagick->clear();
}
// TODO retuns 8-bit BMP
// public function convertToBmpGD(string $pngPath, string $bmpPath): void
// {
// // Load the PNG image
// $image = imagecreatefrompng($pngPath);
//
// // Create a new true color image with the same dimensions
// $bwImage = imagecreatetruecolor(imagesx($image), imagesy($image));
//
// // Convert to black and white
// imagefilter($image, IMG_FILTER_GRAYSCALE);
//
// // Copy the grayscale image to the new image
// imagecopy($bwImage, $image, 0, 0, 0, 0, imagesx($image), imagesy($image));
//
// // Create a 1-bit palette
// imagetruecolortopalette($bwImage, true, 2);
//
// // Save as BMP
//
// imagebmp($bwImage, $bmpPath, false);
//
// // Free up memory
// imagedestroy($image);
// imagedestroy($bwImage);
// }
public function cleanupFolder(): void
{
$activeImageUuids = Device::pluck('current_screen_image')->filter()->toArray();
$files = Storage::disk('public')->files('/images/generated/');
foreach ($files as $file) {
if (basename($file) === '.gitignore') {
continue;
}
// Get filename without path and extension
$fileUuid = pathinfo($file, PATHINFO_FILENAME);
// If the UUID is not in use by any device, move it to archive
if (! in_array($fileUuid, $activeImageUuids)) {
Storage::disk('public')->delete($file);
}
}
} }
} }

View file

@ -0,0 +1,95 @@
<?php
namespace App\Jobs;
use App\Models\Device;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage;
class FetchProxyCloudResponses implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
/**
* Execute the job.
*/
public function handle(): void
{
Device::where('proxy_cloud', true)->each(function ($device) {
try {
$response = Http::withHeaders([
'id' => $device->mac_address,
'access-token' => $device->api_key,
'width' => 800,
'height' => 480,
'rssi' => $device->last_rssi_level,
'battery_voltage' => $device->last_battery_voltage,
'refresh-rate' => $device->default_refresh_interval,
'fw-version' => $device->last_firmware_version,
'accept-encoding' => 'identity;q=1,chunked;q=0.1,*;q=0',
'user-agent' => 'ESP32HTTPClient',
])->get(config('services.trmnl.proxy_base_url').'/api/display');
$device->update([
'proxy_cloud_response' => $response->json(),
]);
$imageUrl = $response->json('image_url');
$filename = $response->json('filename');
\Log::info('Response data: '.$imageUrl);
if (isset($imageUrl)) {
try {
$imageContents = Http::get($imageUrl)->body();
if (! Storage::disk('public')->exists("images/generated/{$filename}.bmp")) {
Storage::disk('public')->put(
"images/generated/{$filename}.bmp",
$imageContents
);
}
$device->update([
'current_screen_image' => $filename,
]);
} catch (\Exception $e) {
Log::error("Failed to download and save image for device: {$device->mac_address}", [
'error' => $e->getMessage(),
]);
}
}
Log::info("Successfully updated proxy cloud response for device: {$device->mac_address}");
if ($device->last_log_request) {
Http::withHeaders([
'id' => $device->mac_address,
'access-token' => $device->api_key,
'width' => 800,
'height' => 480,
'rssi' => $device->last_rssi_level,
'battery_voltage' => $device->last_battery_voltage,
'refresh-rate' => $device->default_refresh_interval,
'fw-version' => $device->last_firmware_version,
'accept-encoding' => 'identity;q=1,chunked;q=0.1,*;q=0',
'user-agent' => 'ESP32HTTPClient',
])->post(config('services.trmnl.proxy_base_url').'/api/log', $device->last_log_request);
$device->update([
'last_log_request' => null,
]);
}
} catch (\Exception $e) {
Log::error("Failed to fetch proxy cloud response for device: {$device->mac_address}", [
'error' => $e->getMessage(),
]);
}
});
}
}

View file

@ -0,0 +1,89 @@
<?php
namespace App\Jobs;
use App\Models\Device;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Storage;
use Ramsey\Uuid\Uuid;
use Spatie\Browsershot\Browsershot;
class GenerateScreenJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
/**
* Create a new job instance.
*/
public function __construct(
private readonly int $deviceId,
private readonly string $markup
) {}
/**
* Execute the job.
*/
public function handle(): void
{
$uuid = Uuid::uuid4()->toString();
$pngPath = Storage::disk('public')->path('/images/generated/'.$uuid.'.png');
$bmpPath = Storage::disk('public')->path('/images/generated/'.$uuid.'.bmp');
// Generate PNG
try {
Browsershot::html($this->markup)
->setOption('args', config('app.puppeteer_docker') ? ['--no-sandbox', '--disable-setuid-sandbox', '--disable-gpu'] : [])
->windowSize(800, 480)
->save($pngPath);
} catch (\Exception $e) {
throw new \RuntimeException('Failed to generate PNG: '.$e->getMessage(), 0, $e);
}
try {
$this->convertToBmpImageMagick($pngPath, $bmpPath);
} catch (\ImagickException $e) {
throw new \RuntimeException('Failed to convert image to BMP: '.$e->getMessage(), 0, $e);
}
Device::find($this->deviceId)->update(['current_screen_image' => $uuid]);
\Log::info("Device $this->deviceId: updated with new image: $uuid");
$this->cleanupFolder();
}
/**
* @throws \ImagickException
*/
private function convertToBmpImageMagick(string $pngPath, string $bmpPath): void
{
$imagick = new \Imagick($pngPath);
$imagick->setImageType(\Imagick::IMGTYPE_GRAYSCALE);
$imagick->quantizeImage(2, \Imagick::COLORSPACE_GRAY, 0, true, false);
$imagick->setImageDepth(1);
$imagick->stripImage();
$imagick->setFormat('BMP3');
$imagick->writeImage($bmpPath);
$imagick->clear();
}
private function cleanupFolder(): void
{
$activeImageUuids = Device::pluck('current_screen_image')->filter()->toArray();
$files = Storage::disk('public')->files('/images/generated/');
foreach ($files as $file) {
if (basename($file) === '.gitignore') {
continue;
}
// Get filename without path and extension
$fileUuid = pathinfo($file, PATHINFO_FILENAME);
// If the UUID is not in use by any device, move it to archive
if (! in_array($fileUuid, $activeImageUuids)) {
Storage::disk('public')->delete($file);
}
}
}
}

View file

@ -0,0 +1,37 @@
<?php
namespace App\Livewire\Actions;
use Livewire\Component;
class DeviceAutoJoin extends Component
{
public bool $deviceAutojoin = false;
public bool $isFirstUser = false;
public function mount()
{
$this->deviceAutojoin = auth()->user()->assign_new_devices;
$this->isFirstUser = auth()->user()->id === 1;
}
public function updating($name, $value)
{
$this->validate([
'deviceAutojoin' => 'boolean',
]);
if ($name === 'deviceAutojoin') {
auth()->user()->update([
'assign_new_devices' => $value,
]);
}
}
public function render()
{
return view('livewire.actions.device-auto-join');
}
}

View file

@ -1,55 +0,0 @@
<?php
namespace App\Livewire;
use App\Models\Device;
use Livewire\Component;
use Livewire\WithPagination;
class DeviceManager extends Component
{
use WithPagination;
public $showDeviceForm = false;
public $name;
public $mac_address;
public $api_key;
public $default_refresh_interval = 900;
public $friendly_id;
protected $rules = [
'mac_address' => 'required',
'api_key' => 'required',
'default_refresh_interval' => 'required|integer',
];
public function render()
{
return view('livewire.device-manager', [
'devices' => auth()->user()->devices()->paginate(10),
]);
}
public function createDevice(): void
{
$this->validate();
Device::factory([
'name' => $this->name,
'mac_address' => $this->mac_address,
'api_key' => $this->api_key,
'default_refresh_interval' => $this->default_refresh_interval,
'friendly_id' => $this->friendly_id,
'user_id' => auth()->id(),
])->create();
$this->reset();
\Flux::modal('create-device')->close();
session()->flash('message', 'Device created successfully.');
}
}

View file

@ -10,4 +10,44 @@ class Device extends Model
use HasFactory; use HasFactory;
protected $guarded = ['id']; protected $guarded = ['id'];
protected $casts = [
'proxy_cloud' => 'boolean',
'last_log_request' => 'json',
];
public function getBatteryPercentAttribute()
{
$volts = $this->last_battery_voltage;
// Define min and max voltage for Li-ion battery (3.0V empty, 4.2V full)
$min_volt = 3.0;
$max_volt = 4.2;
// Ensure the voltage is within range
if ($volts <= $min_volt) {
return 0;
} elseif ($volts >= $max_volt) {
return 100;
}
// Calculate percentage
$percent = (($volts - $min_volt) / ($max_volt - $min_volt)) * 100;
return round($percent);
}
public function getWifiStrenghAttribute()
{
$rssi = $this->last_rssi_level;
if ($rssi >= 0) {
return 0; // No signal (0 bars)
} elseif ($rssi <= -80) {
return 1; // Weak signal (1 bar)
} elseif ($rssi <= -60) {
return 2; // Moderate signal (2 bars)
} else {
return 3; // Strong signal (3 bars)
}
}
} }

View file

@ -24,6 +24,7 @@ class User extends Authenticatable // implements MustVerifyEmail
'name', 'name',
'email', 'email',
'password', 'password',
'assign_new_devices',
]; ];
/** /**
@ -46,6 +47,7 @@ class User extends Authenticatable // implements MustVerifyEmail
return [ return [
'email_verified_at' => 'datetime', 'email_verified_at' => 'datetime',
'password' => 'hashed', 'password' => 'hashed',
'assign_new_devices' => 'boolean',
]; ];
} }

View file

@ -19,6 +19,8 @@ class AppServiceProvider extends ServiceProvider
*/ */
public function boot(): void public function boot(): void
{ {
// if (app()->isProduction() && config('app.force_https')) {
\URL::forceScheme('https');
}
} }
} }

View file

@ -3,6 +3,8 @@
use Illuminate\Foundation\Application; use Illuminate\Foundation\Application;
use Illuminate\Foundation\Configuration\Exceptions; use Illuminate\Foundation\Configuration\Exceptions;
use Illuminate\Foundation\Configuration\Middleware; use Illuminate\Foundation\Configuration\Middleware;
use Laravel\Sanctum\Http\Middleware\CheckAbilities;
use Laravel\Sanctum\Http\Middleware\CheckForAnyAbility;
return Application::configure(basePath: dirname(__DIR__)) return Application::configure(basePath: dirname(__DIR__))
->withRouting( ->withRouting(
@ -12,7 +14,10 @@ return Application::configure(basePath: dirname(__DIR__))
health: '/up', health: '/up',
) )
->withMiddleware(function (Middleware $middleware) { ->withMiddleware(function (Middleware $middleware) {
// $middleware->alias([
'abilities' => CheckAbilities::class,
'ability' => CheckForAnyAbility::class,
]);
}) })
->withExceptions(function (Exceptions $exceptions) { ->withExceptions(function (Exceptions $exceptions) {
// //

114
composer.lock generated
View file

@ -87,16 +87,16 @@
}, },
{ {
"name": "brick/math", "name": "brick/math",
"version": "0.12.1", "version": "0.12.2",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/brick/math.git", "url": "https://github.com/brick/math.git",
"reference": "f510c0a40911935b77b86859eb5223d58d660df1" "reference": "901eddb1e45a8e0f689302e40af871c181ecbe40"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/brick/math/zipball/f510c0a40911935b77b86859eb5223d58d660df1", "url": "https://api.github.com/repos/brick/math/zipball/901eddb1e45a8e0f689302e40af871c181ecbe40",
"reference": "f510c0a40911935b77b86859eb5223d58d660df1", "reference": "901eddb1e45a8e0f689302e40af871c181ecbe40",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -105,7 +105,7 @@
"require-dev": { "require-dev": {
"php-coveralls/php-coveralls": "^2.2", "php-coveralls/php-coveralls": "^2.2",
"phpunit/phpunit": "^10.1", "phpunit/phpunit": "^10.1",
"vimeo/psalm": "5.16.0" "vimeo/psalm": "6.8.8"
}, },
"type": "library", "type": "library",
"autoload": { "autoload": {
@ -135,7 +135,7 @@
], ],
"support": { "support": {
"issues": "https://github.com/brick/math/issues", "issues": "https://github.com/brick/math/issues",
"source": "https://github.com/brick/math/tree/0.12.1" "source": "https://github.com/brick/math/tree/0.12.2"
}, },
"funding": [ "funding": [
{ {
@ -143,7 +143,7 @@
"type": "github" "type": "github"
} }
], ],
"time": "2023-11-29T23:19:16+00:00" "time": "2025-02-26T10:21:45+00:00"
}, },
{ {
"name": "carbonphp/carbon-doctrine-types", "name": "carbonphp/carbon-doctrine-types",
@ -2295,16 +2295,16 @@
}, },
{ {
"name": "livewire/flux", "name": "livewire/flux",
"version": "v2.0.2", "version": "v2.0.3",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/livewire/flux.git", "url": "https://github.com/livewire/flux.git",
"reference": "424d88f7e1c68730edc56fd8041568c135c3d8ab" "reference": "dec010f09419cd9d9930abc4b304802c379be57e"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/livewire/flux/zipball/424d88f7e1c68730edc56fd8041568c135c3d8ab", "url": "https://api.github.com/repos/livewire/flux/zipball/dec010f09419cd9d9930abc4b304802c379be57e",
"reference": "424d88f7e1c68730edc56fd8041568c135c3d8ab", "reference": "dec010f09419cd9d9930abc4b304802c379be57e",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -2352,22 +2352,22 @@
], ],
"support": { "support": {
"issues": "https://github.com/livewire/flux/issues", "issues": "https://github.com/livewire/flux/issues",
"source": "https://github.com/livewire/flux/tree/v2.0.2" "source": "https://github.com/livewire/flux/tree/v2.0.3"
}, },
"time": "2025-02-21T13:00:37+00:00" "time": "2025-02-26T00:29:58+00:00"
}, },
{ {
"name": "livewire/livewire", "name": "livewire/livewire",
"version": "v3.5.20", "version": "v3.6.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/livewire/livewire.git", "url": "https://github.com/livewire/livewire.git",
"reference": "509f2258c51741f6d06deb65d4437654520694e6" "reference": "4c66b569cb729ba9b2f4a3c4e79f50fd8f2b71d1"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/livewire/livewire/zipball/509f2258c51741f6d06deb65d4437654520694e6", "url": "https://api.github.com/repos/livewire/livewire/zipball/4c66b569cb729ba9b2f4a3c4e79f50fd8f2b71d1",
"reference": "509f2258c51741f6d06deb65d4437654520694e6", "reference": "4c66b569cb729ba9b2f4a3c4e79f50fd8f2b71d1",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -2422,7 +2422,7 @@
"description": "A front-end framework for Laravel.", "description": "A front-end framework for Laravel.",
"support": { "support": {
"issues": "https://github.com/livewire/livewire/issues", "issues": "https://github.com/livewire/livewire/issues",
"source": "https://github.com/livewire/livewire/tree/v3.5.20" "source": "https://github.com/livewire/livewire/tree/v3.6.0"
}, },
"funding": [ "funding": [
{ {
@ -2430,7 +2430,7 @@
"type": "github" "type": "github"
} }
], ],
"time": "2025-02-13T21:05:24+00:00" "time": "2025-02-26T00:57:32+00:00"
}, },
{ {
"name": "livewire/volt", "name": "livewire/volt",
@ -4287,16 +4287,16 @@
}, },
{ {
"name": "symfony/error-handler", "name": "symfony/error-handler",
"version": "v7.2.3", "version": "v7.2.4",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/error-handler.git", "url": "https://github.com/symfony/error-handler.git",
"reference": "959a74d044a6db21f4caa6d695648dcb5584cb49" "reference": "aabf79938aa795350c07ce6464dd1985607d95d5"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/symfony/error-handler/zipball/959a74d044a6db21f4caa6d695648dcb5584cb49", "url": "https://api.github.com/repos/symfony/error-handler/zipball/aabf79938aa795350c07ce6464dd1985607d95d5",
"reference": "959a74d044a6db21f4caa6d695648dcb5584cb49", "reference": "aabf79938aa795350c07ce6464dd1985607d95d5",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -4342,7 +4342,7 @@
"description": "Provides tools to manage errors and ease debugging PHP code", "description": "Provides tools to manage errors and ease debugging PHP code",
"homepage": "https://symfony.com", "homepage": "https://symfony.com",
"support": { "support": {
"source": "https://github.com/symfony/error-handler/tree/v7.2.3" "source": "https://github.com/symfony/error-handler/tree/v7.2.4"
}, },
"funding": [ "funding": [
{ {
@ -4358,7 +4358,7 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2025-01-07T09:39:55+00:00" "time": "2025-02-02T20:27:07+00:00"
}, },
{ {
"name": "symfony/event-dispatcher", "name": "symfony/event-dispatcher",
@ -4660,16 +4660,16 @@
}, },
{ {
"name": "symfony/http-kernel", "name": "symfony/http-kernel",
"version": "v7.2.3", "version": "v7.2.4",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/http-kernel.git", "url": "https://github.com/symfony/http-kernel.git",
"reference": "caae9807f8e25a9b43ce8cc6fafab6cf91f0cc9b" "reference": "9f1103734c5789798fefb90e91de4586039003ed"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/symfony/http-kernel/zipball/caae9807f8e25a9b43ce8cc6fafab6cf91f0cc9b", "url": "https://api.github.com/repos/symfony/http-kernel/zipball/9f1103734c5789798fefb90e91de4586039003ed",
"reference": "caae9807f8e25a9b43ce8cc6fafab6cf91f0cc9b", "reference": "9f1103734c5789798fefb90e91de4586039003ed",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -4754,7 +4754,7 @@
"description": "Provides a structured process for converting a Request into a Response", "description": "Provides a structured process for converting a Request into a Response",
"homepage": "https://symfony.com", "homepage": "https://symfony.com",
"support": { "support": {
"source": "https://github.com/symfony/http-kernel/tree/v7.2.3" "source": "https://github.com/symfony/http-kernel/tree/v7.2.4"
}, },
"funding": [ "funding": [
{ {
@ -4770,7 +4770,7 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2025-01-29T07:40:13+00:00" "time": "2025-02-26T11:01:22+00:00"
}, },
{ {
"name": "symfony/mailer", "name": "symfony/mailer",
@ -4854,16 +4854,16 @@
}, },
{ {
"name": "symfony/mime", "name": "symfony/mime",
"version": "v7.2.3", "version": "v7.2.4",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/mime.git", "url": "https://github.com/symfony/mime.git",
"reference": "2fc3b4bd67e4747e45195bc4c98bea4628476204" "reference": "87ca22046b78c3feaff04b337f33b38510fd686b"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/symfony/mime/zipball/2fc3b4bd67e4747e45195bc4c98bea4628476204", "url": "https://api.github.com/repos/symfony/mime/zipball/87ca22046b78c3feaff04b337f33b38510fd686b",
"reference": "2fc3b4bd67e4747e45195bc4c98bea4628476204", "reference": "87ca22046b78c3feaff04b337f33b38510fd686b",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -4918,7 +4918,7 @@
"mime-type" "mime-type"
], ],
"support": { "support": {
"source": "https://github.com/symfony/mime/tree/v7.2.3" "source": "https://github.com/symfony/mime/tree/v7.2.4"
}, },
"funding": [ "funding": [
{ {
@ -4934,7 +4934,7 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2025-01-27T11:08:17+00:00" "time": "2025-02-19T08:51:20+00:00"
}, },
{ {
"name": "symfony/polyfill-ctype", "name": "symfony/polyfill-ctype",
@ -5574,16 +5574,16 @@
}, },
{ {
"name": "symfony/process", "name": "symfony/process",
"version": "v7.2.0", "version": "v7.2.4",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/process.git", "url": "https://github.com/symfony/process.git",
"reference": "d34b22ba9390ec19d2dd966c40aa9e8462f27a7e" "reference": "d8f411ff3c7ddc4ae9166fb388d1190a2df5b5cf"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/symfony/process/zipball/d34b22ba9390ec19d2dd966c40aa9e8462f27a7e", "url": "https://api.github.com/repos/symfony/process/zipball/d8f411ff3c7ddc4ae9166fb388d1190a2df5b5cf",
"reference": "d34b22ba9390ec19d2dd966c40aa9e8462f27a7e", "reference": "d8f411ff3c7ddc4ae9166fb388d1190a2df5b5cf",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -5615,7 +5615,7 @@
"description": "Executes commands in sub-processes", "description": "Executes commands in sub-processes",
"homepage": "https://symfony.com", "homepage": "https://symfony.com",
"support": { "support": {
"source": "https://github.com/symfony/process/tree/v7.2.0" "source": "https://github.com/symfony/process/tree/v7.2.4"
}, },
"funding": [ "funding": [
{ {
@ -5631,7 +5631,7 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2024-11-06T14:24:19+00:00" "time": "2025-02-05T08:33:46+00:00"
}, },
{ {
"name": "symfony/routing", "name": "symfony/routing",
@ -5886,16 +5886,16 @@
}, },
{ {
"name": "symfony/translation", "name": "symfony/translation",
"version": "v7.2.2", "version": "v7.2.4",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/translation.git", "url": "https://github.com/symfony/translation.git",
"reference": "e2674a30132b7cc4d74540d6c2573aa363f05923" "reference": "283856e6981286cc0d800b53bd5703e8e363f05a"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/symfony/translation/zipball/e2674a30132b7cc4d74540d6c2573aa363f05923", "url": "https://api.github.com/repos/symfony/translation/zipball/283856e6981286cc0d800b53bd5703e8e363f05a",
"reference": "e2674a30132b7cc4d74540d6c2573aa363f05923", "reference": "283856e6981286cc0d800b53bd5703e8e363f05a",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -5961,7 +5961,7 @@
"description": "Provides tools to internationalize your application", "description": "Provides tools to internationalize your application",
"homepage": "https://symfony.com", "homepage": "https://symfony.com",
"support": { "support": {
"source": "https://github.com/symfony/translation/tree/v7.2.2" "source": "https://github.com/symfony/translation/tree/v7.2.4"
}, },
"funding": [ "funding": [
{ {
@ -5977,7 +5977,7 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2024-12-07T08:18:10+00:00" "time": "2025-02-13T10:27:23+00:00"
}, },
{ {
"name": "symfony/translation-contracts", "name": "symfony/translation-contracts",
@ -8268,23 +8268,23 @@
}, },
{ {
"name": "phpunit/php-code-coverage", "name": "phpunit/php-code-coverage",
"version": "11.0.8", "version": "11.0.9",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/sebastianbergmann/php-code-coverage.git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git",
"reference": "418c59fd080954f8c4aa5631d9502ecda2387118" "reference": "14d63fbcca18457e49c6f8bebaa91a87e8e188d7"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/418c59fd080954f8c4aa5631d9502ecda2387118", "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/14d63fbcca18457e49c6f8bebaa91a87e8e188d7",
"reference": "418c59fd080954f8c4aa5631d9502ecda2387118", "reference": "14d63fbcca18457e49c6f8bebaa91a87e8e188d7",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
"ext-dom": "*", "ext-dom": "*",
"ext-libxml": "*", "ext-libxml": "*",
"ext-xmlwriter": "*", "ext-xmlwriter": "*",
"nikic/php-parser": "^5.3.1", "nikic/php-parser": "^5.4.0",
"php": ">=8.2", "php": ">=8.2",
"phpunit/php-file-iterator": "^5.1.0", "phpunit/php-file-iterator": "^5.1.0",
"phpunit/php-text-template": "^4.0.1", "phpunit/php-text-template": "^4.0.1",
@ -8296,7 +8296,7 @@
"theseer/tokenizer": "^1.2.3" "theseer/tokenizer": "^1.2.3"
}, },
"require-dev": { "require-dev": {
"phpunit/phpunit": "^11.5.0" "phpunit/phpunit": "^11.5.2"
}, },
"suggest": { "suggest": {
"ext-pcov": "PHP extension that provides line coverage", "ext-pcov": "PHP extension that provides line coverage",
@ -8334,7 +8334,7 @@
"support": { "support": {
"issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues",
"security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy",
"source": "https://github.com/sebastianbergmann/php-code-coverage/tree/11.0.8" "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/11.0.9"
}, },
"funding": [ "funding": [
{ {
@ -8342,7 +8342,7 @@
"type": "github" "type": "github"
} }
], ],
"time": "2024-12-11T12:34:27+00:00" "time": "2025-02-25T13:26:39+00:00"
}, },
{ {
"name": "phpunit/php-file-iterator", "name": "phpunit/php-file-iterator",

View file

@ -123,4 +123,11 @@ return [
'store' => env('APP_MAINTENANCE_STORE', 'database'), 'store' => env('APP_MAINTENANCE_STORE', 'database'),
], ],
'registration' => [
'enabled' => env('REGISTRATION_ENABLED', true),
],
'force_https' => env('FORCE_HTTPS', false),
'puppeteer_docker' => env('PUPPETEER_DOCKER', false),
]; ];

View file

@ -94,6 +94,17 @@ return [
'processors' => [PsrLogMessageProcessor::class], 'processors' => [PsrLogMessageProcessor::class],
], ],
'stdout' => [
'driver' => 'monolog',
'level' => env('LOG_LEVEL', 'debug'),
'handler' => StreamHandler::class,
'formatter' => env('LOG_STDOUT_FORMATTER'),
'with' => [
'stream' => 'php://stdout',
],
'processors' => [PsrLogMessageProcessor::class],
],
'stderr' => [ 'stderr' => [
'driver' => 'monolog', 'driver' => 'monolog',
'level' => env('LOG_LEVEL', 'debug'), 'level' => env('LOG_LEVEL', 'debug'),

View file

@ -35,4 +35,9 @@ return [
], ],
], ],
'trmnl' => [
'proxy_base_url' => env('TRMNL_PROXY_BASE_URL', 'https://trmnl.app'),
'proxy_refresh_minutes' => env('TRMNL_PROXY_REFRESH_MINUTES', 15),
],
]; ];

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->boolean('assign_new_devices')->default(false);
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('users', function (Blueprint $table) {
$table->dropColumn('assign_new_devices');
});
}
};

View file

@ -0,0 +1,29 @@
<?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('devices', function (Blueprint $table) {
$table->boolean('proxy_cloud')->default(false);
$table->text('proxy_cloud_response')->nullable();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('devices', function (Blueprint $table) {
$table->dropColumn(['proxy_cloud', 'proxy_cloud_response']);
});
}
};

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('devices', function (Blueprint $table) {
$table->json('last_log_request')->nullable();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('devices', function (Blueprint $table) {
$table->dropColumn('last_log_request');
});
}
};

View file

@ -2,7 +2,6 @@
namespace Database\Seeders; namespace Database\Seeders;
use App\Models\Device;
use App\Models\User; use App\Models\User;
// use Illuminate\Database\Console\Seeds\WithoutModelEvents; // use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder; use Illuminate\Database\Seeder;

15
docker-compose.yml Normal file
View file

@ -0,0 +1,15 @@
services:
app:
build:
context: .
dockerfile: Dockerfile
ports:
- "4567:80"
environment:
#- APP_KEY=
- TRMNL_PROXY_REFRESH_MINUTES=15
# volumes:
# - ./database/database.sqlite:/var/www/html/database/database.sqlite
# - ./storage:/var/www/html/storage
restart: unless-stopped
#platform: "linux/arm64/v8"

17
docker/nginx.conf Normal file
View file

@ -0,0 +1,17 @@
server {
listen 80;
server_name _;
root /var/www/html/public;
index index.php;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ~ \.php$ {
fastcgi_pass 127.0.0.1:9000;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
}
}

14
docker/php.ini Normal file
View file

@ -0,0 +1,14 @@
[PHP]
memory_limit = 256M
max_execution_time = 60
upload_max_filesize = 50M
post_max_size = 50M
[opcache]
opcache.enable=1
opcache.memory_consumption=128
opcache.interned_strings_buffer=8
opcache.max_accelerated_files=4000
opcache.revalidate_freq=60
opcache.fast_shutdown=1
opcache.enable_cli=1

56
docker/supervisord.conf Normal file
View file

@ -0,0 +1,56 @@
[supervisord]
nodaemon=true
user=root
logfile=/var/log/supervisor/supervisord.log
pidfile=/var/run/supervisord.pid
[program:php-fpm]
command=php-fpm
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0
[program:nginx]
command=nginx -g 'daemon off;'
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0
[program:laravel-queue]
command=php /var/www/html/artisan queue:work
autostart=true
autorestart=true
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0
stopwaitsecs=3600
[program:laravel-scheduler]
command=php /var/www/html/artisan schedule:work
autostart=true
autorestart=true
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0
[program:laravel-setup]
command=/bin/sh -c "php /var/www/html/artisan storage:link >> /tmp/storage-link.done"
autostart=true
autorestart=false
startsecs=0
exitcodes=0
stdout_logfile=/dev/stdout
stderr_logfile=/dev/stderr
[program:laravel-db-migrate]
command=/bin/sh -c "php /var/www/html/artisan migrate --force >> /tmp/migrate.done"
autostart=true
autorestart=false
startsecs=0
exitcodes=0
stdout_logfile=/dev/stdout
stderr_logfile=/dev/stderr

View file

@ -20,15 +20,17 @@
:current="request()->routeIs('devices')"> :current="request()->routeIs('devices')">
Devices Devices
</flux:navbar.item> </flux:navbar.item>
<flux:navbar.item icon="puzzle-piece" href="{{ route('plugins.index') }}" wire:navigate
:current="request()->routeIs(['plugins.index', 'plugins.markup'])">
Plugins
</flux:navbar.item>
</flux:navbar> </flux:navbar>
<flux:spacer/> <flux:spacer/>
{{-- <flux:navbar class="mr-1.5 space-x-0.5 py-0!">--}} <flux:navbar class="mr-1.5 space-x-0.5 py-0! max-lg:hidden">
{{-- <flux:tooltip content="Add devices automatically that try to connect to this server" position="bottom">--}} <livewire:actions.device-auto-join/>
{{-- <flux:switch --}}{{-- wire:model.live="device-autojoin" --}}{{-- label="Permit Auto-Join"/>--}} </flux:navbar>
{{-- </flux:tooltip>--}}
{{-- </flux:navbar>--}}
<!-- Desktop User Menu --> <!-- Desktop User Menu -->
<flux:dropdown position="top" align="end"> <flux:dropdown position="top" align="end">
@ -87,22 +89,24 @@
<flux:navlist variant="outline"> <flux:navlist variant="outline">
<flux:navlist.group heading="Platform"> <flux:navlist.group heading="Platform">
<flux:navlist.item icon="layout-grid" href="{{ route('dashboard') }}" wire:navigate <flux:navlist.item icon="layout-grid" href="{{ route('dashboard') }}" wire:navigate
:current="request()->routeIs('dashboard')"> :current="request()->routeIs('dashboard')" class="m-2">
Dashboard Dashboard
</flux:navlist.item> </flux:navlist.item>
<flux:navbar.item icon="square-chart-gantt" href="{{ route('devices') }}" wire:navigate <flux:navbar.item icon="square-chart-gantt" href="{{ route('devices') }}" wire:navigate
:current="request()->routeIs('devices')"> :current="request()->routeIs('devices')" class="m-2">
Devices Devices
</flux:navbar.item> </flux:navbar.item>
<flux:navbar.item icon="puzzle-piece" href="{{ route('plugins.index') }}" wire:navigate
:current="request()->routeIs('plugins.index')" class="m-2">
Plugins
</flux:navbar.item>
</flux:navlist.group> </flux:navlist.group>
</flux:navlist> </flux:navlist>
<flux:spacer/> <flux:spacer/>
<flux:navlist variant="outline"> <flux:navlist variant="outline">
{{-- <flux:navlist.item icon="folder-git-2" href="https://github.com/laravel/livewire-starter-kit" target="_blank">--}} <livewire:actions.device-auto-join/>
{{-- Repository--}}
{{-- </flux:navlist.item>--}}
</flux:navlist> </flux:navlist>
</flux:sidebar> </flux:sidebar>

View file

@ -0,0 +1,10 @@
@props(['percent'])
<flux:tooltip content="Battery Percent: {{ $percent }}%" position="bottom">
@if ($percent > 60)
<flux:icon.battery-full class="dark:text-zinc-200"/>
@elseif ($percent < 20)
<flux:icon.battery-low class="dark:text-zinc-200"/>
@else
<flux:icon.battery-medium class="dark:text-zinc-200"/>
@endif
</flux:tooltip>

View file

@ -0,0 +1,12 @@
@props(['strength', 'rssi'])
<flux:tooltip content="Wi-Fi RSSI Level: {{ $rssi }} db" position="bottom">
@if ($strength === 3)
<flux:icon.wifi class="dark:text-zinc-200"/>
@elseif ($strength === 2)
<flux:icon.wifi-high class="dark:text-zinc-200"/>
@elseif ($strength === 1)
<flux:icon.wifi-low class="dark:text-zinc-200"/>
@else
<flux:icon.wifi-zero class="dark:text-zinc-200"/>
@endif
</flux:tooltip>

View file

@ -1,5 +0,0 @@
<x-layouts.app>
<div class="bg-muted flex flex-col items-center justify-center gap-6 p-6 md:p-10">
<livewire:device-dashboard/>
</div>
</x-layouts.app>

View file

@ -1,3 +0,0 @@
<x-layouts.app>
<livewire:device-manager />
</x-layouts.app>

View file

@ -1,63 +0,0 @@
<x-layouts.app>
<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="rounded-xl border bg-white dark:bg-stone-950 dark:border-stone-800 text-stone-800 shadow-xs">
<div class="px-10 py-8">
@php
$current_image_uuid =$device->current_screen_image;
$current_image_path = 'images/generated/' . $current_image_uuid . '.png';
@endphp
<h1 class="text-xl font-medium dark:text-zinc-200">{{ $device->name }}</h1>
<p class="text-sm dark:text-zinc-400">{{$device->mac_address}}</p>
<p class="text-sm dark:text-zinc-400">Friendly Id: {{$device->friendly_id}}</p>
<p class="text-sm dark:text-zinc-400">Refresh Interval: {{$device->default_refresh_interval}}</p>
<p class="text-sm dark:text-zinc-400">Battery Voltage: {{$device->last_battery_voltage}}</p>
<p class="text-sm dark:text-zinc-400">Wifi RSSI Level: {{$device->last_rssi_level}}</p>
<p class="text-sm dark:text-zinc-400">Firmware Version: {{$device->last_firmware_version}}</p>
<flux:input.group class="mt-4 mb-2">
<flux:input.group.prefix>API Key</flux:input.group.prefix>
<flux:input icon="key" value="{{ $device->api_key }}" type="password" viewable class="max-w-xs"/>
</flux:input.group>
@if($current_image_uuid)
<flux:separator class="mt-6 mb-6" text="Current Screen" />
<img src="{{ asset($current_image_path) }}" alt="Current Image"/>
@endif
</div>
</div>
</div>
</div>
</x-layouts.app>
{{--<x-layouts.app>--}}
{{-- <x-slot name="header">--}}
{{-- <h2 class="font-semibold text-xl text-gray-800 dark:text-gray-200 leading-tight">--}}
{{-- {{ __('Device Details: ') }} {{ $device->name }}--}}
{{-- </h2>--}}
{{-- </x-slot>--}}
{{-- <div class="py-12">--}}
{{-- <div class="max-w-7xl mx-auto sm:px-6 lg:px-8">--}}
{{-- <div class="bg-white dark:bg-gray-800 overflow-hidden shadow-xl sm:rounded-lg p-6">--}}
{{-- <div class="mb-4">--}}
{{-- <p class="dark:text-gray-100"><strong>Name</strong> {{ $device->name }}</p>--}}
{{-- <p class="dark:text-gray-100"><strong>Friendly ID</strong> {{ $device->friendly_id }}</p>--}}
{{-- <p class="dark:text-gray-100"><strong>Mac Address</strong> {{ $device->mac_address }}</p>--}}
{{-- <p><strong>API Key</strong> <flux:input value="{{ $device->api_key }}" type="password" viewable></flux:input></p>--}}
{{-- <p class="dark:text-gray-100"><strong>Refresh--}}
{{-- Interval</strong> {{ $device->default_refresh_interval }}</p>--}}
{{-- <p class="dark:text-gray-100"><strong>Battery Voltage</strong> {{ $device->last_battery_voltage }}--}}
{{-- </p>--}}
{{-- <p class="dark:text-gray-100"><strong>Wifi RSSI Level</strong> {{ $device->last_rssi_level }}</p>--}}
{{-- <p class="dark:text-gray-100"><strong>Firmware Version</strong> {{ $device->last_firmware_version }}--}}
{{-- </p>--}}
{{-- </div>--}}
{{-- @if($image)--}}
{{-- <img src="{{$image}}"/>--}}
{{-- @endif--}}
{{-- </div>--}}
{{-- </div>--}}
{{-- </div>--}}
{{--</x-layouts.app>--}}

View file

@ -0,0 +1,41 @@
{{-- Credit: Lucide (https://lucide.dev) --}}
@props([
'variant' => 'outline',
])
@php
if ($variant === 'solid') {
throw new \Exception('The "solid" variant is not supported in Lucide.');
}
$classes = Flux::classes('shrink-0')
->add(match($variant) {
'outline' => '[:where(&)]:size-6',
'solid' => '[:where(&)]:size-6',
'mini' => '[:where(&)]:size-5',
'micro' => '[:where(&)]:size-4',
});
$strokeWidth = match ($variant) {
'outline' => 2,
'mini' => 2.25,
'micro' => 2.5,
};
@endphp
<svg
{{ $attributes->class($classes) }}
data-flux-icon
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="{{ $strokeWidth }}"
stroke-linecap="round"
stroke-linejoin="round"
aria-hidden="true"
data-slot="icon"
>
<path d="M12 22a7 7 0 0 0 7-7c0-2-1-3.9-3-5.5s-3.5-4-4-6.5c-.5 2.5-2 4.9-4 6.5C6 11.1 5 13 5 15a7 7 0 0 0 7 7z" />
</svg>

View file

@ -0,0 +1,7 @@
<div>
@if($isFirstUser)
<flux:tooltip content="Add devices automatically that try to connect to this server" position="bottom">
<flux:switch wire:model.live="deviceAutojoin" label="Permit Auto-Join"/>
</flux:tooltip>
@endif
</div>

View file

@ -80,7 +80,8 @@ new #[Layout('components.layouts.auth')] class extends Component {
<form wire:submit="login" class="flex flex-col gap-6"> <form wire:submit="login" class="flex flex-col gap-6">
<!-- Email Address --> <!-- Email Address -->
<flux:input wire:model="email" label="{{ __('Email address') }}" type="email" name="email" required autofocus autocomplete="email" placeholder="email@example.com" /> <flux:input wire:model="email" label="{{ __('Email address') }}" type="email" name="email" required autofocus
autocomplete="email" placeholder="email@example.com"/>
<!-- Password --> <!-- Password -->
<div class="relative"> <div class="relative">
@ -109,8 +110,12 @@ new #[Layout('components.layouts.auth')] class extends Component {
</div> </div>
</form> </form>
@if (Route::has('register'))
<div class="space-x-1 text-center text-sm text-zinc-600 dark:text-zinc-400"> <div class="space-x-1 text-center text-sm text-zinc-600 dark:text-zinc-400">
Don't have an account? Don't have an account?
<x-text-link href="{{ route('register') }}">Sign up</x-text-link> <x-text-link href="{{ route('register') }}">Sign up</x-text-link>
</div> </div>
@endif
</div> </div>

View file

@ -1,3 +1,15 @@
<?php
use Livewire\Volt\Component;
new class extends Component {
public function mount()
{
return view('livewire.device-dashboard', ['devices' => auth()->user()->devices()->paginate(10)]);
}
}
?>
<div> <div>
<div class="flex w-full max-w-3xl flex-col gap-6"> <div class="flex w-full max-w-3xl flex-col gap-6">
@if($devices->isEmpty()) @if($devices->isEmpty())
@ -21,7 +33,8 @@
<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;
$current_image_path = 'images/generated/' . $current_image_uuid . '.png'; file_exists('storage/images/generated/' . $current_image_uuid . '.png') ? $file_extension = 'png' : $file_extension = 'bmp';
$current_image_path = 'storage/images/generated/' . $current_image_uuid . '.' . $file_extension;
@endphp @endphp
<h1 class="text-xl font-medium dark:text-zinc-200">{{ $device->name }}</h1> <h1 class="text-xl font-medium dark:text-zinc-200">{{ $device->name }}</h1>

View file

@ -1,124 +0,0 @@
<div class="py-12">
{{--@dump($devices)--}}
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
<div class="flex justify-between items-center mb-6">
<h2 class="text-2xl font-semibold dark:text-gray-100">Devices</h2>
<flux:modal.trigger name="create-device">
<flux:button icon="plus" variant="primary">Add Device</flux:button>
</flux:modal.trigger>
</div>
@if (session()->has('message'))
<div class="bg-green-100 border border-green-400 text-green-700 px-4 py-3 rounded relative mb-4">
{{ session('message') }}
</div>
@endif
<flux:modal name="create-device" class="md:w-96">
<div class="space-y-6">
<div>
<flux:heading size="lg">Add Device</flux:heading>
</div>
<form wire:submit="createDevice">
<div class="mb-4">
<flux:input label="Name" wire:model="name" id="name" class="block mt-1 w-full" type="text"
name="name"
autofocus/>
</div>
<div class="mb-4">
<flux:input label="Mac Address" wire:model="mac_address" id="mac_address"
class="block mt-1 w-full"
type="text" name="mac_address" autofocus/>
</div>
<div class="mb-4">
<flux:input label="API Key" wire:model="api_key" id="api_key" class="block mt-1 w-full"
type="text"
name="api_key" autofocus/>
</div>
<div class="mb-4">
<flux:input label="Friendly Id" wire:model="friendly_id" id="friendly_id"
class="block mt-1 w-full"
type="text" name="friendly_id" autofocus/>
</div>
<div class="mb-4">
<flux:input label="Refresh Rate (seconds)" wire:model="default_refresh_interval"
id="default_refresh_interval"
class="block mt-1 w-full" type="text" name="default_refresh_interval"
autofocus/>
</div>
<div class="flex">
<flux:spacer/>
<flux:button type="submit" variant="primary">Create Device</flux:button>
</div>
</form>
</div>
</flux:modal>
<table
class="min-w-full table-fixed text-zinc-800 divide-y divide-zinc-800/10 dark:divide-white/20 text-zinc-800"
data-flux-table="">
<thead data-flux-columns="">
<tr>
<th class="py-3 px-3 first:pl-0 last:pr-0 text-left text-sm font-medium text-zinc-800 dark:text-white"
data-flux-column="">
<div class="whitespace-nowrap flex group-[]/right-align:justify-end">Name</div>
</th>
<th class="py-3 px-3 first:pl-0 last:pr-0 text-left text-sm font-medium text-zinc-800 dark:text-white"
data-flux-column="">
<div class="whitespace-nowrap flex group-[]/right-align:justify-end">Friendly ID</div>
</th>
<th class="py-3 px-3 first:pl-0 last:pr-0 text-left text-sm font-medium text-zinc-800 dark:text-white"
data-flux-column="">
<div class="whitespace-nowrap flex group-[]/right-align:justify-end">Mac Address</div>
</th>
<th class="py-3 px-3 first:pl-0 last:pr-0 text-left text-sm font-medium text-zinc-800 dark:text-white"
data-flux-column="">
<div class="whitespace-nowrap flex group-[]/right-align:justify-end">Refresh</div>
</th>
<th class="py-3 px-3 first:pl-0 last:pr-0 text-left text-sm font-medium text-zinc-800 dark:text-white"
data-flux-column="">
<div class="whitespace-nowrap flex group-[]/right-align:justify-end">Actions</div>
</th>
</tr>
</thead>
<tbody class="divide-y divide-zinc-800/10 dark:divide-white/20" data-flux-rows="">
@foreach ($devices as $device)
<tr data-flux-row="">
<td class="py-3 px-3 first:pl-0 last:pr-0 text-sm whitespace-nowrap text-zinc-500 dark:text-zinc-300"
>
{{ $device->name }}
</td>
<td class="py-3 px-3 first:pl-0 last:pr-0 text-sm whitespace-nowrap text-zinc-500 dark:text-zinc-300"
>
{{ $device->friendly_id }}
</td>
<td class="py-3 px-3 first:pl-0 last:pr-0 text-sm whitespace-nowrap text-zinc-500 dark:text-zinc-300"
>
<div type="button" data-flux-badge="data-flux-badge"
class="inline-flex items-center font-medium whitespace-nowrap -mt-1 -mb-1 text-xs py-1 [&_[data-flux-badge-icon]]:size-3 [&_[data-flux-badge-icon]]:mr-1 rounded-md px-2 text-zinc-700 [&_button]:!text-zinc-700 dark:text-zinc-200 [&_button]:dark:!text-zinc-200 bg-zinc-400/15 dark:bg-zinc-400/40 [&:is(button)]:hover:bg-zinc-400/25 [&:is(button)]:hover:dark:bg-zinc-400/50">
{{ $device->mac_address }}
</div>
</td>
<td class="py-3 px-3 first:pl-0 last:pr-0 text-sm whitespace-nowrap text-zinc-500 dark:text-zinc-300"
>
{{ $device->default_refresh_interval }}
</td>
<td class="py-3 px-3 first:pl-0 last:pr-0 text-sm whitespace-nowrap font-medium text-zinc-800 dark:text-white"
>
<flux:button href="{{ route('devices.configure', $device) }}" wire:navigate icon="eye">
</flux:button>
</td>
</tr>
@endforeach
<!--[if ENDBLOCK]><![endif]-->
</tbody>
</table>
</div>
</div>

View file

@ -0,0 +1,120 @@
<?php
use Livewire\Volt\Component;
new class extends Component {
public $device;
public function mount(\App\Models\Device $device)
{
abort_unless(auth()->user()->devices->contains($device), 403);
$current_image_uuid = $device->current_screen_image;
$current_image_path = 'images/generated/' . $current_image_uuid . '.png';
return view('livewire.devices.configure', compact('device'), [
'image' => ($current_image_uuid) ? url($current_image_path) : null,
]);
}
public function deleteDevice(\App\Models\Device $device)
{
abort_unless(auth()->user()->devices->contains($device), 403);
$device->delete();
redirect()->route('devices');
}
}
?>
<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="rounded-xl border bg-white dark:bg-stone-950 dark:border-stone-800 text-stone-800 shadow-xs">
<div class="px-10 py-8 min-w-lg">
@php
$current_image_uuid =$device->current_screen_image;
file_exists('storage/images/generated/' . $current_image_uuid . '.png') ? $file_extension = 'png' : $file_extension = 'bmp';
$current_image_path = 'storage/images/generated/' . $current_image_uuid . '.' . $file_extension;
@endphp
<div class="flex items-center justify-between">
<flux:tooltip content="Friendly ID: {{$device->friendly_id}}" position="bottom">
<h1 class="text-xl font-medium dark:text-zinc-200">{{ $device->name }}</h1>
</flux:tooltip>
<div>
<flux:modal.trigger name="edit-device">
<flux:button icon="key" variant="subtle"/>
</flux:modal.trigger>
<flux:modal.trigger name="delete-device">
<flux:button icon="trash" variant="danger"/>
</flux:modal.trigger>
</div>
<div class="flex gap-2">
<flux:tooltip content="Last update" position="bottom">
<span class="dark:text-zinc-200">{{$device->updated_at->diffForHumans()}}</span>
</flux:tooltip>
<flux:separator vertical/>
<flux:tooltip content="MAC Address" position="bottom">
<span class="dark:text-zinc-200">{{$device->mac_address}}</span>
</flux:tooltip>
<flux:separator vertical/>
<flux:tooltip content="Firmware Version" position="bottom">
<span class="dark:text-zinc-200">{{$device->last_firmware_version}}</span>
</flux:tooltip>
<flux:separator vertical/>
<x-responsive-icons.wifi :strength="$device->wifiStrengh" :rssi="$device->last_rssi_level"
class="dark:text-zinc-200"/>
<flux:separator vertical/>
<x-responsive-icons.battery :percent="$device->batteryPercent"/>
</div>
</div>
<flux:modal name="edit-device" class="md:w-96">
<div class="space-y-6">
<div>
{{-- <flux:heading size="lg">Edit TRMNL</flux:heading>--}}
{{-- <flux:subheading></flux:subheading>--}}
</div>
{{-- <flux:input label="Name" value="{{ $device->name }}"/>--}}
<flux:input label="API Key" icon="key" value="{{ $device->api_key }}" type="password"
viewable class="max-w-xs"/>
<div class="flex">
<flux:spacer/>
{{-- <flux:button type="submit" variant="primary">Save changes</flux:button>--}}
</div>
</div>
</flux:modal>
<flux:modal name="delete-device" class="min-w-[22rem] space-y-6">
<div>
<flux:heading size="lg">Delete {{$device->name}}?</flux:heading>
</div>
<div class="flex gap-2">
<flux:spacer/>
<flux:modal.close>
<flux:button variant="ghost">Cancel</flux:button>
</flux:modal.close>
<flux:button wire:click="deleteDevice({{ $device->id }})" variant="danger">Delete device</flux:button>
</div>
</flux:modal>
@if($current_image_uuid)
<flux:separator class="mt-6 mb-6" text="Next Screen"/>
<img src="{{ asset($current_image_path) }}" alt="Next Image"/>
@endif
</div>
</div>
</div>
</div>

View file

@ -0,0 +1,205 @@
<?php
use App\Models\Device;
use Livewire\Volt\Component;
new class extends Component {
public $devices;
public $showDeviceForm = false;
public $name;
public $mac_address;
public $api_key;
public $default_refresh_interval = 900;
public $friendly_id;
protected $rules = [
'mac_address' => 'required',
'api_key' => 'required',
'default_refresh_interval' => 'required|integer',
];
public function mount()
{
$this->devices = auth()->user()->devices;
return view('livewire.devices.manage');
}
public function createDevice(): void
{
$this->validate();
Device::create([
'name' => $this->name,
'mac_address' => $this->mac_address,
'api_key' => $this->api_key,
'default_refresh_interval' => $this->default_refresh_interval,
'friendly_id' => $this->friendly_id,
'user_id' => auth()->id(),
]);
$this->reset();
\Flux::modal('create-device')->close();
$this->devices = auth()->user()->devices;
session()->flash('message', 'Device created successfully.');
}
public function toggleProxyCloud(Device $device): void
{
abort_unless(auth()->user()->devices->contains($device), 403);
$device->update([
'proxy_cloud' => !$device->proxy_cloud,
]);
// if ($device->proxy_cloud) {
// \App\Jobs\FetchProxyCloudResponses::dispatch();
// }
}
}
?>
<div>
<div class="py-12">
{{--@dump($devices)--}}
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
<div class="flex justify-between items-center mb-6">
<h2 class="text-2xl font-semibold dark:text-gray-100">Devices</h2>
<flux:modal.trigger name="create-device">
<flux:button icon="plus" variant="primary">Add Device</flux:button>
</flux:modal.trigger>
</div>
@if (session()->has('message'))
<div class="bg-green-100 border border-green-400 text-green-700 px-4 py-3 rounded relative mb-4">
{{ session('message') }}
</div>
@endif
<flux:modal name="create-device" class="md:w-96">
<div class="space-y-6">
<div>
<flux:heading size="lg">Add Device</flux:heading>
</div>
<form wire:submit="createDevice">
<div class="mb-4">
<flux:input label="Name" wire:model="name" id="name" class="block mt-1 w-full" type="text"
name="name"
autofocus/>
</div>
<div class="mb-4">
<flux:input label="Mac Address" wire:model="mac_address" id="mac_address"
class="block mt-1 w-full"
type="text" name="mac_address" autofocus/>
</div>
<div class="mb-4">
<flux:input label="API Key" wire:model="api_key" id="api_key" class="block mt-1 w-full"
type="text"
name="api_key" autofocus/>
</div>
<div class="mb-4">
<flux:input label="Friendly Id" wire:model="friendly_id" id="friendly_id"
class="block mt-1 w-full"
type="text" name="friendly_id" autofocus/>
</div>
<div class="mb-4">
<flux:input label="Refresh Rate (seconds)" wire:model="default_refresh_interval"
id="default_refresh_interval"
class="block mt-1 w-full" type="text" name="default_refresh_interval"
autofocus/>
</div>
<div class="flex">
<flux:spacer/>
<flux:button type="submit" variant="primary">Create Device</flux:button>
</div>
</form>
</div>
</flux:modal>
<table
class="min-w-full table-fixed text-zinc-800 divide-y divide-zinc-800/10 dark:divide-white/20 text-zinc-800"
data-flux-table="">
<thead data-flux-columns="">
<tr>
<th class="py-3 px-3 first:pl-0 last:pr-0 text-left text-sm font-medium text-zinc-800 dark:text-white"
data-flux-column="">
<div class="whitespace-nowrap flex group-[]/right-align:justify-end">Name</div>
</th>
<th class="py-3 px-3 first:pl-0 last:pr-0 text-left text-sm font-medium text-zinc-800 dark:text-white"
data-flux-column="">
<div class="whitespace-nowrap flex group-[]/right-align:justify-end">Friendly ID</div>
</th>
<th class="py-3 px-3 first:pl-0 last:pr-0 text-left text-sm font-medium text-zinc-800 dark:text-white"
data-flux-column="">
<div class="whitespace-nowrap flex group-[]/right-align:justify-end">Mac Address</div>
</th>
<th class="py-3 px-3 first:pl-0 last:pr-0 text-left text-sm font-medium text-zinc-800 dark:text-white"
data-flux-column="">
<div class="whitespace-nowrap flex group-[]/right-align:justify-end">Refresh</div>
</th>
<th class="py-3 px-3 first:pl-0 last:pr-0 text-left text-sm font-medium text-zinc-800 dark:text-white"
data-flux-column="">
<div class="whitespace-nowrap flex group-[]/right-align:justify-end">Actions</div>
</th>
</tr>
</thead>
<tbody class="divide-y divide-zinc-800/10 dark:divide-white/20" data-flux-rows="">
@foreach ($devices as $device)
<tr data-flux-row="">
<td class="py-3 px-3 first:pl-0 last:pr-0 text-sm whitespace-nowrap text-zinc-500 dark:text-zinc-300"
>
{{ $device->name }}
</td>
<td class="py-3 px-3 first:pl-0 last:pr-0 text-sm whitespace-nowrap text-zinc-500 dark:text-zinc-300"
>
{{ $device->friendly_id }}
</td>
<td class="py-3 px-3 first:pl-0 last:pr-0 text-sm whitespace-nowrap text-zinc-500 dark:text-zinc-300"
>
<div type="button" data-flux-badge="data-flux-badge"
class="inline-flex items-center font-medium whitespace-nowrap -mt-1 -mb-1 text-xs py-1 [&_[data-flux-badge-icon]]:size-3 [&_[data-flux-badge-icon]]:mr-1 rounded-md px-2 text-zinc-700 [&_button]:!text-zinc-700 dark:text-zinc-200 [&_button]:dark:!text-zinc-200 bg-zinc-400/15 dark:bg-zinc-400/40 [&:is(button)]:hover:bg-zinc-400/25 [&:is(button)]:hover:dark:bg-zinc-400/50">
{{ $device->mac_address }}
</div>
</td>
<td class="py-3 px-3 first:pl-0 last:pr-0 text-sm whitespace-nowrap text-zinc-500 dark:text-zinc-300"
>
{{ $device->default_refresh_interval }}
</td>
<td class="py-3 px-3 first:pl-0 last:pr-0 text-sm whitespace-nowrap font-medium text-zinc-800 dark:text-white"
>
<div class="flex items-center gap-4">
<flux:button href="{{ route('devices.configure', $device) }}" wire:navigate icon="eye">
</flux:button>
<flux:tooltip
content="Proxies images from the TRMNL Cloud service when no image is set (available in TRMNL DEV Edition only)."
position="bottom">
<flux:switch wire:model.live="proxy_cloud"
wire:click="toggleProxyCloud({{ $device->id }})"
:checked="$device->proxy_cloud" label="☁️ Proxy"/>
</flux:tooltip>
</div>
</td>
</tr>
@endforeach
<!--[if ENDBLOCK]><![endif]-->
</tbody>
</table>
</div>
</div>
</div>

View file

@ -0,0 +1,56 @@
<?php
use Livewire\Volt\Component;
new class extends Component {
public $token;
public function mount(): void
{
$token = Auth::user()?->tokens()?->first();
if ($token === null) {
$token = Auth::user()->createToken('api-token', ['update-screen']);
}
$this->token = $token->plainTextToken;
}
public function regenerateToken()
{
Auth::user()->tokens()?->first()?->delete();
$token = Auth::user()->createToken('api-token', ['update-screen']);
$this->token = $token->plainTextToken;
}
};
?>
<div class="py-12">
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
<div class="flex justify-between items-center mb-6">
<h2 class="text-2xl font-semibold dark:text-gray-100">API</h2>
</div>
<div>
<p>
<flux:badge>POST</flux:badge>
<span class="ml-2 font-mono">{{route('display.update')}}</span>
</p>
<div class="mt-4">
<h3 class="text-lg">Headers</h3>
<div>Authorization <span class="ml-2 font-mono">Bearer {{$token ?? '**********'}}</span>
<flux:button variant="subtle" size="xs" class="mt-2" wire:click="regenerateToken()">
Regenerate Token
</flux:button>
</div>
</div>
<div class="mt-4">
<h3 class="text-lg">Body</h3>
<div class="font-mono">
<pre>
{&#x22;markup&#x22;:&#x22;&#x3C;h1&#x3E;Hello World&#x3C;/h1&#x3E;&#x22;}
</pre>
</div>
</div>
</div>
</div>
</div>

View file

@ -0,0 +1,36 @@
<?php
use Livewire\Volt\Component;
new class extends Component {
public $plugins = [
'markup' =>
['name' => 'Markup', 'icon' => 'code-backet', 'route' => 'plugins.markup'],
'api' =>
['name' => 'API', 'icon' => 'code-backet', 'route' => 'plugins.api'],
];
};
?>
<div class="py-12">
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
<div class="flex justify-between items-center mb-6">
<h2 class="text-2xl font-semibold dark:text-gray-100">Plugins</h2>
</div>
<div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-6">
@foreach($plugins as $plugin)
<div
class="rounded-xl border bg-white dark:bg-stone-950 dark:border-stone-800 text-stone-800 shadow-xs">
<a href="{{ route($plugin['route']) }}" class="block">
<div class="flex items-center space-x-4 px-10 py-8">
<flux:icon name="code-bracket" class="text-4xl text-accent"/>
<h3 class="text-xl font-medium dark:text-zinc-200">{{$plugin['name']}}</h3>
</div>
</a>
</div>
@endforeach
</div>
</div>
</div>

View file

@ -0,0 +1,171 @@
<?php
use App\Jobs\GenerateScreenJob;
use Livewire\Volt\Component;
new class extends Component {
public string $blade_code = '';
public bool $isLoading = false;
public function submit()
{
$this->isLoading = true;
$this->validate([
'blade_code' => 'required|string'
]);
try {
$rendered = Blade::render($this->blade_code);
// if (config('app.puppeteer_docker')) {
// GenerateScreenJob::dispatch(auth()->user()->devices()->first()->id, $rendered);
// } else {
GenerateScreenJob::dispatchSync(auth()->user()->devices()->first()->id, $rendered);
// }
} catch (\Exception $e) {
$this->addError('error', $e->getMessage());
}
$this->isLoading = false;
}
public function renderExample(string $example)
{
switch ($example) {
case 'quote':
$markup = $this->renderQuote();
break;
case 'trainMonitor':
$markup = $this->renderTrainMonitor();
break;
case 'homeAssistant':
$markup = $this->renderHomeAssistant();
break;
default:
$markup = '<h1>Hello World!</h1>';
break;
}
$this->blade_code = $markup;
}
public function renderQuote(): string
{
return <<<HTML
<x-trmnl::view>
<x-trmnl::layout>
<x-trmnl::markdown gapSize="large">
<x-trmnl::title>Motivational Quote</x-trmnl::title>
<x-trmnl::content>“I love inside jokes. I hope to be a part of one someday.</x-trmnl::content>
<x-trmnl::label variant="underline">Michael Scott</x-trmnl::label>
</x-trmnl::markdown>
</x-trmnl::layout>
<x-trmnl::title-bar/>
</x-trmnl::view>
HTML;
}
public function renderTrainMonitor()
{
return <<<HTML
<x-trmnl::view>
<x-trmnl::layout>
<x-trmnl::table>
<thead>
<tr>
<th><x-trmnl::title>Abfahrt</x-trmnl::title></th>
<th><x-trmnl::title>Aktuell</x-trmnl::title></th>
<th><x-trmnl::title>Zug</x-trmnl::title></th>
<th><x-trmnl::title>Ziel</x-trmnl::title></th>
<th><x-trmnl::title>Steig</x-trmnl::title></th>
</tr>
</thead>
<tbody>
<tr>
<td><x-trmnl::label>08:51</x-trmnl::label></td>
<td><x-trmnl::label>08:52</x-trmnl::label></td>
<td><x-trmnl::label>REX 1</x-trmnl::label></td>
<td><x-trmnl::label>Vienna Main Station</x-trmnl::label></td>
<td><x-trmnl::label>3</x-trmnl::label></td>
</tr>
</tbody>
</x-trmnl::table>
</x-trmnl::layout>
<x-trmnl::title-bar title="Train Monitor"/>
</x-trmnl::view>
HTML;
}
public function renderHomeAssistant()
{
return <<<HTML
<x-trmnl::view>
<x-trmnl::layout class="layout--col gap--space-between">
<x-trmnl::grid cols="4">
<x-trmnl::col position="center">
<x-trmnl::item>
<x-trmnl::meta/>
<x-trmnl::content>
<x-trmnl::value size="large">23.3°</x-trmnl::value>
<x-trmnl::label class="w--full flex">
<flux:icon icon="droplet"/>
47.52 %
</x-trmnl::label>
<x-trmnl::label class="w--full flex">Sensor 1</x-trmnl::label>
</x-trmnl::content>
</x-trmnl::item>
</x-trmnl::col>
</x-trmnl::grid>
</x-trmnl::layout>
<x-trmnl::title-bar title="Home Assistant"/>
</x-trmnl::view>
HTML;
}
};
?>
<div class="py-12">
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
<h2 class="text-2xl font-semibold dark:text-gray-100">Markup</h2>
{{-- <div class="flex justify-between items-center mb-6">--}}
<div class="mt-5 mb-5 ">
<span>Examples</span>
<div class="text-accent">
<a href="#" wire:click="renderExample('quote')" class="text-xl">Quote</a> |
<a href="#" wire:click="renderExample('trainMonitor')" class="text-xl">Train Monitor</a> |
<a href="#" wire:click="renderExample('homeAssistant')" class="text-xl">Temperature Sensors</a>
</div>
</div>
<form wire:submit="submit">
<div class="mb-4">
<flux:textarea
label="Blade Code"
class="font-mono"
wire:model="blade_code"
id="blade_code"
name="blade_code"
rows="15"
placeholder="Enter your blade code here..."
/>
</div>
<div class="flex">
<flux:spacer/>
<flux:button type="submit" variant="primary">
Generate Screen
</flux:button>
</div>
</form>
{{-- </div>--}}
</div>
</div>

View file

@ -1,22 +1,40 @@
<?php <?php
use App\Jobs\GenerateScreenJob;
use App\Models\Device; use App\Models\Device;
use App\Models\User;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route; use Illuminate\Support\Facades\Route;
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', $mac_address) $device = Device::where('mac_address', $mac_address)
->where('api_key', $access_token) ->where('api_key', $access_token)
->first(); ->first();
if (! $device) { 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) {
// Create a new device and assign it to this user
$device = Device::create([
'mac_address' => $mac_address,
'api_key' => $access_token,
'user_id' => $auto_assign_user->id,
'name' => "{$auto_assign_user->name}'s TRMNL",
'friendly_id' => Str::random(6),
'default_refresh_interval' => 900,
]);
} else {
return response()->json([ return response()->json([
'message' => 'MAC Address not registered or invalid access token', 'message' => 'MAC Address not registered or invalid access token',
], 404); ], 404);
} }
}
$device->update([ $device->update([
'last_rssi_level' => $request->header('rssi'), 'last_rssi_level' => $request->header('rssi'),
@ -25,9 +43,13 @@ Route::get('/display', function (Request $request) {
]); ]);
$image_uuid = $device->current_screen_image; $image_uuid = $device->current_screen_image;
if (! $image_uuid) {
$image_path = 'images/setup-logo.bmp';
$filename = 'setup-logo.bmp';
} else {
$image_path = 'images/generated/'.$image_uuid.'.bmp'; $image_path = 'images/generated/'.$image_uuid.'.bmp';
$filename = basename($image_path); $filename = basename($image_path);
}
return response()->json([ return response()->json([
'status' => '0', 'status' => '0',
@ -67,8 +89,24 @@ Route::get('/setup', function (Request $request) {
}); });
Route::post('/log', function (Request $request) { Route::post('/log', function (Request $request) {
$logs = $request->json('log.logs_array', []); $mac_address = $request->header('id');
$access_token = $request->header('access-token');
$device = Device::where('mac_address', $mac_address)
->where('api_key', $access_token)
->first();
if (! $device) {
return response()->json([
'message' => 'Device not found or invalid access token',
], 404);
}
$device->update([
'last_log_request' => $request->json()->all(),
]);
$logs = $request->json('log.logs_array', []);
foreach ($logs as $log) { foreach ($logs as $log) {
\Log::info('Device Log', $log); \Log::info('Device Log', $log);
} }
@ -81,3 +119,23 @@ Route::post('/log', function (Request $request) {
Route::get('/user', function (Request $request) { Route::get('/user', function (Request $request) {
return $request->user(); return $request->user();
})->middleware('auth:sanctum'); })->middleware('auth:sanctum');
Route::post('/display/update', function (Request $request) {
$request->validate([
'device_id' => 'required|exists:devices,id',
'markup' => 'required|string',
]);
$deviceId = $request['device_id'];
abort_unless($request->user()->devices->contains($deviceId), 403);
$view = Blade::render($request['markup']);
GenerateScreenJob::dispatchSync($deviceId, $view);
response()->json([
'message' => 'success',
]);
})
->name('display.update')
->middleware('auth:sanctum', 'ability:update-screen');

View file

@ -8,8 +8,10 @@ Route::middleware('guest')->group(function () {
Volt::route('login', 'auth.login') Volt::route('login', 'auth.login')
->name('login'); ->name('login');
if (config('app.registration.enabled')) {
Volt::route('register', 'auth.register') Volt::route('register', 'auth.register')
->name('register'); ->name('register');
}
Volt::route('forgot-password', 'auth.forgot-password') Volt::route('forgot-password', 'auth.forgot-password')
->name('password.request'); ->name('password.request');

View file

@ -1,8 +1,9 @@
<?php <?php
use Illuminate\Foundation\Inspiring; use App\Jobs\FetchProxyCloudResponses;
use Illuminate\Support\Facades\Artisan;
// Artisan::command('inspire', function () { // Artisan::command('inspire', function () {
// $this->comment(Inspiring::quote()); // $this->comment(Inspiring::quote());
// })->purpose('Display an inspiring quote')->hourly(); // })->purpose('Display an inspiring quote')->hourly();
Schedule::job(new FetchProxyCloudResponses)->everyFifteenMinutes();

View file

@ -7,30 +7,21 @@ Route::get('/', function () {
return view('welcome'); return view('welcome');
})->name('home'); })->name('home');
Route::view('dashboard', 'dashboard')
->middleware(['auth', 'verified'])
->name('dashboard');
Route::middleware(['auth'])->group(function () { Route::middleware(['auth'])->group(function () {
Route::redirect('settings', 'settings/profile'); Route::redirect('settings', 'settings/profile');
Volt::route('settings/profile', 'settings.profile')->name('settings.profile'); Volt::route('settings/profile', 'settings.profile')->name('settings.profile');
Volt::route('settings/password', 'settings.password')->name('settings.password'); Volt::route('settings/password', 'settings.password')->name('settings.password');
Volt::route('settings/appearance', 'settings.appearance')->name('settings.appearance'); Volt::route('settings/appearance', 'settings.appearance')->name('settings.appearance');
Route::get('/devices', function () { Volt::route('/dashboard', 'device-dashboard')->name('dashboard');
return view('devices');
})->name('devices');
Route::get('/devices/{device}/configure', function (App\Models\Device $device) { Volt::route('/devices', 'devices.manage')->name('devices');
$current_image_uuid = auth()->user()->devices()->find($device->id)->current_screen_image; Volt::route('/devices/{device}/configure', 'devices.configure')->name('devices.configure');
$current_image_path = 'images/generated/' . $current_image_uuid . '.png';
return view('devices.configure', compact('device'), [ Volt::route('plugins', 'plugins.index')->name('plugins.index');
'image' => ($current_image_uuid) ? url($current_image_path) : null,
]);
})->name('devices.configure');
Volt::route('plugins/markup', 'plugins.markup')->name('plugins.markup');
Volt::route('plugins/api', 'plugins.api')->name('plugins.api');
}); });
require __DIR__.'/auth.php'; require __DIR__.'/auth.php';