diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..32d49fe --- /dev/null +++ b/.dockerignore @@ -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 diff --git a/.env.example b/.env.example index 6fb3de6..227ccb4 100644 --- a/.env.example +++ b/.env.example @@ -1,7 +1,7 @@ -APP_NAME=Laravel -APP_ENV=local -APP_KEY= -APP_DEBUG=true +APP_NAME=TrmnlServer +APP_ENV=production +APP_KEY=base64:zzPXBQPlgn0NHwVBTVG0B//8P/PVwVnBp2gk0ZWR0+k= +APP_DEBUG=false APP_TIMEZONE=UTC APP_URL=http://localhost @@ -17,7 +17,7 @@ PHP_CLI_SERVER_WORKERS=4 BCRYPT_ROUNDS=12 LOG_CHANNEL=stack -LOG_STACK=single +LOG_STACK=single,stderr LOG_DEPRECATIONS_CHANNEL=null LOG_LEVEL=debug @@ -64,3 +64,7 @@ AWS_BUCKET= AWS_USE_PATH_STYLE_ENDPOINT=false VITE_APP_NAME="${APP_NAME}" + +TRMNL_PROXY_BASE_URL=https://trmnl.app +TRMNL_PROXY_REFRESH_MINUTES=15 +REGISTRATION_ENABLED=1 diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..6b0786e --- /dev/null +++ b/Dockerfile @@ -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"] diff --git a/app/Console/Commands/FetchProxyCloudResponsesCommand.php b/app/Console/Commands/FetchProxyCloudResponsesCommand.php new file mode 100644 index 0000000..f182cd6 --- /dev/null +++ b/app/Console/Commands/FetchProxyCloudResponsesCommand.php @@ -0,0 +1,18 @@ +argument('deviceId'); $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 { - Browsershot::html(view($view)->render()) - ->windowSize(800, 480) - ->save($pngPath); - } catch (\Exception $e) { - $this->error('Failed to generate PNG: '.$e->getMessage()); + $markup = view($view)->render(); + } catch (\Throwable $e) { + $this->error('Failed to render view: '.$e->getMessage()); - return; + return 1; } - try { - $this->convertToBmpImageMagick($pngPath, $bmpPath); + GenerateScreenJob::dispatchSync($deviceId, $markup); - } catch (\ImagickException $e) { - $this->error('Failed to convert image to BMP: '.$e->getMessage()); - } + $this->info('Screen generation job finished.'); - Device::find($deviceId)->update(['current_screen_image' => $uuid]); - - $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); - } - } + return 0; } } diff --git a/app/Jobs/FetchProxyCloudResponses.php b/app/Jobs/FetchProxyCloudResponses.php new file mode 100644 index 0000000..31fb5bf --- /dev/null +++ b/app/Jobs/FetchProxyCloudResponses.php @@ -0,0 +1,95 @@ +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(), + ]); + } + }); + } +} diff --git a/app/Jobs/GenerateScreenJob.php b/app/Jobs/GenerateScreenJob.php new file mode 100644 index 0000000..e1d0d25 --- /dev/null +++ b/app/Jobs/GenerateScreenJob.php @@ -0,0 +1,89 @@ +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); + } + } + } +} diff --git a/app/Livewire/Actions/DeviceAutoJoin.php b/app/Livewire/Actions/DeviceAutoJoin.php new file mode 100644 index 0000000..c16322c --- /dev/null +++ b/app/Livewire/Actions/DeviceAutoJoin.php @@ -0,0 +1,37 @@ +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'); + } +} diff --git a/app/Livewire/DeviceManager.php b/app/Livewire/DeviceManager.php deleted file mode 100644 index 68faf49..0000000 --- a/app/Livewire/DeviceManager.php +++ /dev/null @@ -1,55 +0,0 @@ - '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.'); - } -} diff --git a/app/Models/Device.php b/app/Models/Device.php index a455c5e..d4b5745 100644 --- a/app/Models/Device.php +++ b/app/Models/Device.php @@ -10,4 +10,44 @@ class Device extends Model use HasFactory; 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) + } + } } diff --git a/app/Models/User.php b/app/Models/User.php index 33fcc54..7e7da3b 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -24,6 +24,7 @@ class User extends Authenticatable // implements MustVerifyEmail 'name', 'email', 'password', + 'assign_new_devices', ]; /** @@ -46,6 +47,7 @@ class User extends Authenticatable // implements MustVerifyEmail return [ 'email_verified_at' => 'datetime', 'password' => 'hashed', + 'assign_new_devices' => 'boolean', ]; } diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index 452e6b6..6609fa8 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -19,6 +19,8 @@ class AppServiceProvider extends ServiceProvider */ public function boot(): void { - // + if (app()->isProduction() && config('app.force_https')) { + \URL::forceScheme('https'); + } } } diff --git a/bootstrap/app.php b/bootstrap/app.php index d654276..fccf967 100644 --- a/bootstrap/app.php +++ b/bootstrap/app.php @@ -3,6 +3,8 @@ use Illuminate\Foundation\Application; use Illuminate\Foundation\Configuration\Exceptions; use Illuminate\Foundation\Configuration\Middleware; +use Laravel\Sanctum\Http\Middleware\CheckAbilities; +use Laravel\Sanctum\Http\Middleware\CheckForAnyAbility; return Application::configure(basePath: dirname(__DIR__)) ->withRouting( @@ -12,7 +14,10 @@ return Application::configure(basePath: dirname(__DIR__)) health: '/up', ) ->withMiddleware(function (Middleware $middleware) { - // + $middleware->alias([ + 'abilities' => CheckAbilities::class, + 'ability' => CheckForAnyAbility::class, + ]); }) ->withExceptions(function (Exceptions $exceptions) { // diff --git a/composer.lock b/composer.lock index 7c60762..c853906 100644 --- a/composer.lock +++ b/composer.lock @@ -87,16 +87,16 @@ }, { "name": "brick/math", - "version": "0.12.1", + "version": "0.12.2", "source": { "type": "git", "url": "https://github.com/brick/math.git", - "reference": "f510c0a40911935b77b86859eb5223d58d660df1" + "reference": "901eddb1e45a8e0f689302e40af871c181ecbe40" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/brick/math/zipball/f510c0a40911935b77b86859eb5223d58d660df1", - "reference": "f510c0a40911935b77b86859eb5223d58d660df1", + "url": "https://api.github.com/repos/brick/math/zipball/901eddb1e45a8e0f689302e40af871c181ecbe40", + "reference": "901eddb1e45a8e0f689302e40af871c181ecbe40", "shasum": "" }, "require": { @@ -105,7 +105,7 @@ "require-dev": { "php-coveralls/php-coveralls": "^2.2", "phpunit/phpunit": "^10.1", - "vimeo/psalm": "5.16.0" + "vimeo/psalm": "6.8.8" }, "type": "library", "autoload": { @@ -135,7 +135,7 @@ ], "support": { "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": [ { @@ -143,7 +143,7 @@ "type": "github" } ], - "time": "2023-11-29T23:19:16+00:00" + "time": "2025-02-26T10:21:45+00:00" }, { "name": "carbonphp/carbon-doctrine-types", @@ -2295,16 +2295,16 @@ }, { "name": "livewire/flux", - "version": "v2.0.2", + "version": "v2.0.3", "source": { "type": "git", "url": "https://github.com/livewire/flux.git", - "reference": "424d88f7e1c68730edc56fd8041568c135c3d8ab" + "reference": "dec010f09419cd9d9930abc4b304802c379be57e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/livewire/flux/zipball/424d88f7e1c68730edc56fd8041568c135c3d8ab", - "reference": "424d88f7e1c68730edc56fd8041568c135c3d8ab", + "url": "https://api.github.com/repos/livewire/flux/zipball/dec010f09419cd9d9930abc4b304802c379be57e", + "reference": "dec010f09419cd9d9930abc4b304802c379be57e", "shasum": "" }, "require": { @@ -2352,22 +2352,22 @@ ], "support": { "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", - "version": "v3.5.20", + "version": "v3.6.0", "source": { "type": "git", "url": "https://github.com/livewire/livewire.git", - "reference": "509f2258c51741f6d06deb65d4437654520694e6" + "reference": "4c66b569cb729ba9b2f4a3c4e79f50fd8f2b71d1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/livewire/livewire/zipball/509f2258c51741f6d06deb65d4437654520694e6", - "reference": "509f2258c51741f6d06deb65d4437654520694e6", + "url": "https://api.github.com/repos/livewire/livewire/zipball/4c66b569cb729ba9b2f4a3c4e79f50fd8f2b71d1", + "reference": "4c66b569cb729ba9b2f4a3c4e79f50fd8f2b71d1", "shasum": "" }, "require": { @@ -2422,7 +2422,7 @@ "description": "A front-end framework for Laravel.", "support": { "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": [ { @@ -2430,7 +2430,7 @@ "type": "github" } ], - "time": "2025-02-13T21:05:24+00:00" + "time": "2025-02-26T00:57:32+00:00" }, { "name": "livewire/volt", @@ -4287,16 +4287,16 @@ }, { "name": "symfony/error-handler", - "version": "v7.2.3", + "version": "v7.2.4", "source": { "type": "git", "url": "https://github.com/symfony/error-handler.git", - "reference": "959a74d044a6db21f4caa6d695648dcb5584cb49" + "reference": "aabf79938aa795350c07ce6464dd1985607d95d5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/error-handler/zipball/959a74d044a6db21f4caa6d695648dcb5584cb49", - "reference": "959a74d044a6db21f4caa6d695648dcb5584cb49", + "url": "https://api.github.com/repos/symfony/error-handler/zipball/aabf79938aa795350c07ce6464dd1985607d95d5", + "reference": "aabf79938aa795350c07ce6464dd1985607d95d5", "shasum": "" }, "require": { @@ -4342,7 +4342,7 @@ "description": "Provides tools to manage errors and ease debugging PHP code", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/error-handler/tree/v7.2.3" + "source": "https://github.com/symfony/error-handler/tree/v7.2.4" }, "funding": [ { @@ -4358,7 +4358,7 @@ "type": "tidelift" } ], - "time": "2025-01-07T09:39:55+00:00" + "time": "2025-02-02T20:27:07+00:00" }, { "name": "symfony/event-dispatcher", @@ -4660,16 +4660,16 @@ }, { "name": "symfony/http-kernel", - "version": "v7.2.3", + "version": "v7.2.4", "source": { "type": "git", "url": "https://github.com/symfony/http-kernel.git", - "reference": "caae9807f8e25a9b43ce8cc6fafab6cf91f0cc9b" + "reference": "9f1103734c5789798fefb90e91de4586039003ed" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-kernel/zipball/caae9807f8e25a9b43ce8cc6fafab6cf91f0cc9b", - "reference": "caae9807f8e25a9b43ce8cc6fafab6cf91f0cc9b", + "url": "https://api.github.com/repos/symfony/http-kernel/zipball/9f1103734c5789798fefb90e91de4586039003ed", + "reference": "9f1103734c5789798fefb90e91de4586039003ed", "shasum": "" }, "require": { @@ -4754,7 +4754,7 @@ "description": "Provides a structured process for converting a Request into a Response", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/http-kernel/tree/v7.2.3" + "source": "https://github.com/symfony/http-kernel/tree/v7.2.4" }, "funding": [ { @@ -4770,7 +4770,7 @@ "type": "tidelift" } ], - "time": "2025-01-29T07:40:13+00:00" + "time": "2025-02-26T11:01:22+00:00" }, { "name": "symfony/mailer", @@ -4854,16 +4854,16 @@ }, { "name": "symfony/mime", - "version": "v7.2.3", + "version": "v7.2.4", "source": { "type": "git", "url": "https://github.com/symfony/mime.git", - "reference": "2fc3b4bd67e4747e45195bc4c98bea4628476204" + "reference": "87ca22046b78c3feaff04b337f33b38510fd686b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/mime/zipball/2fc3b4bd67e4747e45195bc4c98bea4628476204", - "reference": "2fc3b4bd67e4747e45195bc4c98bea4628476204", + "url": "https://api.github.com/repos/symfony/mime/zipball/87ca22046b78c3feaff04b337f33b38510fd686b", + "reference": "87ca22046b78c3feaff04b337f33b38510fd686b", "shasum": "" }, "require": { @@ -4918,7 +4918,7 @@ "mime-type" ], "support": { - "source": "https://github.com/symfony/mime/tree/v7.2.3" + "source": "https://github.com/symfony/mime/tree/v7.2.4" }, "funding": [ { @@ -4934,7 +4934,7 @@ "type": "tidelift" } ], - "time": "2025-01-27T11:08:17+00:00" + "time": "2025-02-19T08:51:20+00:00" }, { "name": "symfony/polyfill-ctype", @@ -5574,16 +5574,16 @@ }, { "name": "symfony/process", - "version": "v7.2.0", + "version": "v7.2.4", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "d34b22ba9390ec19d2dd966c40aa9e8462f27a7e" + "reference": "d8f411ff3c7ddc4ae9166fb388d1190a2df5b5cf" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/d34b22ba9390ec19d2dd966c40aa9e8462f27a7e", - "reference": "d34b22ba9390ec19d2dd966c40aa9e8462f27a7e", + "url": "https://api.github.com/repos/symfony/process/zipball/d8f411ff3c7ddc4ae9166fb388d1190a2df5b5cf", + "reference": "d8f411ff3c7ddc4ae9166fb388d1190a2df5b5cf", "shasum": "" }, "require": { @@ -5615,7 +5615,7 @@ "description": "Executes commands in sub-processes", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/process/tree/v7.2.0" + "source": "https://github.com/symfony/process/tree/v7.2.4" }, "funding": [ { @@ -5631,7 +5631,7 @@ "type": "tidelift" } ], - "time": "2024-11-06T14:24:19+00:00" + "time": "2025-02-05T08:33:46+00:00" }, { "name": "symfony/routing", @@ -5886,16 +5886,16 @@ }, { "name": "symfony/translation", - "version": "v7.2.2", + "version": "v7.2.4", "source": { "type": "git", "url": "https://github.com/symfony/translation.git", - "reference": "e2674a30132b7cc4d74540d6c2573aa363f05923" + "reference": "283856e6981286cc0d800b53bd5703e8e363f05a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/translation/zipball/e2674a30132b7cc4d74540d6c2573aa363f05923", - "reference": "e2674a30132b7cc4d74540d6c2573aa363f05923", + "url": "https://api.github.com/repos/symfony/translation/zipball/283856e6981286cc0d800b53bd5703e8e363f05a", + "reference": "283856e6981286cc0d800b53bd5703e8e363f05a", "shasum": "" }, "require": { @@ -5961,7 +5961,7 @@ "description": "Provides tools to internationalize your application", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/translation/tree/v7.2.2" + "source": "https://github.com/symfony/translation/tree/v7.2.4" }, "funding": [ { @@ -5977,7 +5977,7 @@ "type": "tidelift" } ], - "time": "2024-12-07T08:18:10+00:00" + "time": "2025-02-13T10:27:23+00:00" }, { "name": "symfony/translation-contracts", @@ -8268,23 +8268,23 @@ }, { "name": "phpunit/php-code-coverage", - "version": "11.0.8", + "version": "11.0.9", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "418c59fd080954f8c4aa5631d9502ecda2387118" + "reference": "14d63fbcca18457e49c6f8bebaa91a87e8e188d7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/418c59fd080954f8c4aa5631d9502ecda2387118", - "reference": "418c59fd080954f8c4aa5631d9502ecda2387118", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/14d63fbcca18457e49c6f8bebaa91a87e8e188d7", + "reference": "14d63fbcca18457e49c6f8bebaa91a87e8e188d7", "shasum": "" }, "require": { "ext-dom": "*", "ext-libxml": "*", "ext-xmlwriter": "*", - "nikic/php-parser": "^5.3.1", + "nikic/php-parser": "^5.4.0", "php": ">=8.2", "phpunit/php-file-iterator": "^5.1.0", "phpunit/php-text-template": "^4.0.1", @@ -8296,7 +8296,7 @@ "theseer/tokenizer": "^1.2.3" }, "require-dev": { - "phpunit/phpunit": "^11.5.0" + "phpunit/phpunit": "^11.5.2" }, "suggest": { "ext-pcov": "PHP extension that provides line coverage", @@ -8334,7 +8334,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", "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": [ { @@ -8342,7 +8342,7 @@ "type": "github" } ], - "time": "2024-12-11T12:34:27+00:00" + "time": "2025-02-25T13:26:39+00:00" }, { "name": "phpunit/php-file-iterator", diff --git a/config/app.php b/config/app.php index f467267..92fe477 100644 --- a/config/app.php +++ b/config/app.php @@ -123,4 +123,11 @@ return [ 'store' => env('APP_MAINTENANCE_STORE', 'database'), ], + 'registration' => [ + 'enabled' => env('REGISTRATION_ENABLED', true), + ], + + 'force_https' => env('FORCE_HTTPS', false), + 'puppeteer_docker' => env('PUPPETEER_DOCKER', false), + ]; diff --git a/config/logging.php b/config/logging.php index 8d94292..47b1d08 100644 --- a/config/logging.php +++ b/config/logging.php @@ -94,6 +94,17 @@ return [ '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' => [ 'driver' => 'monolog', 'level' => env('LOG_LEVEL', 'debug'), diff --git a/config/services.php b/config/services.php index 27a3617..7e05c6b 100644 --- a/config/services.php +++ b/config/services.php @@ -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), + ], + ]; diff --git a/database/migrations/2025_02_26_095239_add_assign_new_devices_to_users_table.php b/database/migrations/2025_02_26_095239_add_assign_new_devices_to_users_table.php new file mode 100644 index 0000000..20f33ae --- /dev/null +++ b/database/migrations/2025_02_26_095239_add_assign_new_devices_to_users_table.php @@ -0,0 +1,28 @@ +boolean('assign_new_devices')->default(false); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('users', function (Blueprint $table) { + $table->dropColumn('assign_new_devices'); + }); + } +}; diff --git a/database/migrations/2025_02_26_144104_add_proxy_cloud_columns_to_devices_table.php b/database/migrations/2025_02_26_144104_add_proxy_cloud_columns_to_devices_table.php new file mode 100644 index 0000000..7435d62 --- /dev/null +++ b/database/migrations/2025_02_26_144104_add_proxy_cloud_columns_to_devices_table.php @@ -0,0 +1,29 @@ +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']); + }); + } +}; diff --git a/database/migrations/2025_02_27_151530_add_last_log_request_to_devices_table.php b/database/migrations/2025_02_27_151530_add_last_log_request_to_devices_table.php new file mode 100644 index 0000000..45ed5be --- /dev/null +++ b/database/migrations/2025_02_27_151530_add_last_log_request_to_devices_table.php @@ -0,0 +1,28 @@ +json('last_log_request')->nullable(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('devices', function (Blueprint $table) { + $table->dropColumn('last_log_request'); + }); + } +}; diff --git a/database/seeders/DatabaseSeeder.php b/database/seeders/DatabaseSeeder.php index 0d43e89..75f3991 100644 --- a/database/seeders/DatabaseSeeder.php +++ b/database/seeders/DatabaseSeeder.php @@ -2,7 +2,6 @@ namespace Database\Seeders; -use App\Models\Device; use App\Models\User; // use Illuminate\Database\Console\Seeds\WithoutModelEvents; use Illuminate\Database\Seeder; diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..d5fa69e --- /dev/null +++ b/docker-compose.yml @@ -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" diff --git a/docker/nginx.conf b/docker/nginx.conf new file mode 100644 index 0000000..5f42b71 --- /dev/null +++ b/docker/nginx.conf @@ -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; + } +} \ No newline at end of file diff --git a/docker/php.ini b/docker/php.ini new file mode 100644 index 0000000..1b12c3e --- /dev/null +++ b/docker/php.ini @@ -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 \ No newline at end of file diff --git a/docker/supervisord.conf b/docker/supervisord.conf new file mode 100644 index 0000000..07352b4 --- /dev/null +++ b/docker/supervisord.conf @@ -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 diff --git a/resources/views/components/layouts/app/header.blade.php b/resources/views/components/layouts/app/header.blade.php index bcaa805..513798d 100644 --- a/resources/views/components/layouts/app/header.blade.php +++ b/resources/views/components/layouts/app/header.blade.php @@ -20,15 +20,17 @@ :current="request()->routeIs('devices')"> Devices + + Plugins + - {{-- --}} - {{-- --}} - {{-- --}} - {{-- --}} - {{-- --}} + + + @@ -87,22 +89,24 @@ + :current="request()->routeIs('dashboard')" class="m-2"> Dashboard + :current="request()->routeIs('devices')" class="m-2"> Devices + + Plugins + - {{-- --}} - {{-- Repository--}} - {{-- --}} + diff --git a/resources/views/components/responsive-icons/battery.blade.php b/resources/views/components/responsive-icons/battery.blade.php new file mode 100644 index 0000000..9371de8 --- /dev/null +++ b/resources/views/components/responsive-icons/battery.blade.php @@ -0,0 +1,10 @@ +@props(['percent']) + + @if ($percent > 60) + + @elseif ($percent < 20) + + @else + + @endif + diff --git a/resources/views/components/responsive-icons/wifi.blade.php b/resources/views/components/responsive-icons/wifi.blade.php new file mode 100644 index 0000000..cbfbc84 --- /dev/null +++ b/resources/views/components/responsive-icons/wifi.blade.php @@ -0,0 +1,12 @@ +@props(['strength', 'rssi']) + + @if ($strength === 3) + + @elseif ($strength === 2) + + @elseif ($strength === 1) + + @else + + @endif + diff --git a/resources/views/dashboard.blade.php b/resources/views/dashboard.blade.php deleted file mode 100644 index 9985214..0000000 --- a/resources/views/dashboard.blade.php +++ /dev/null @@ -1,5 +0,0 @@ - -
- -
-
diff --git a/resources/views/devices.blade.php b/resources/views/devices.blade.php deleted file mode 100644 index 6b9fb7c..0000000 --- a/resources/views/devices.blade.php +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/resources/views/devices/configure.blade.php b/resources/views/devices/configure.blade.php deleted file mode 100644 index 44f0d66..0000000 --- a/resources/views/devices/configure.blade.php +++ /dev/null @@ -1,63 +0,0 @@ - -
-
-
-
- @php - $current_image_uuid =$device->current_screen_image; - $current_image_path = 'images/generated/' . $current_image_uuid . '.png'; - @endphp - -

{{ $device->name }}

-

{{$device->mac_address}}

-

Friendly Id: {{$device->friendly_id}}

-

Refresh Interval: {{$device->default_refresh_interval}}

-

Battery Voltage: {{$device->last_battery_voltage}}

-

Wifi RSSI Level: {{$device->last_rssi_level}}

-

Firmware Version: {{$device->last_firmware_version}}

- - API Key - - - @if($current_image_uuid) - - Current Image - @endif -
-
-
-
-
- - -{{----}} -{{-- --}} -{{--

--}} -{{-- {{ __('Device Details: ') }} {{ $device->name }}--}} -{{--

--}} -{{--
--}} - -{{--
--}} -{{--
--}} -{{--
--}} -{{--
--}} -{{--

Name {{ $device->name }}

--}} -{{--

Friendly ID {{ $device->friendly_id }}

--}} -{{--

Mac Address {{ $device->mac_address }}

--}} -{{--

API Key

--}} -{{--

Refresh--}} -{{-- Interval {{ $device->default_refresh_interval }}

--}} -{{--

Battery Voltage {{ $device->last_battery_voltage }}--}} -{{--

--}} -{{--

Wifi RSSI Level {{ $device->last_rssi_level }}

--}} -{{--

Firmware Version {{ $device->last_firmware_version }}--}} -{{--

--}} -{{--
--}} -{{-- @if($image)--}} -{{-- --}} -{{-- @endif--}} -{{--
--}} -{{--
--}} -{{--
--}} -{{--
--}} diff --git a/resources/views/flux/icon/droplet.blade.php b/resources/views/flux/icon/droplet.blade.php new file mode 100644 index 0000000..98c0cd1 --- /dev/null +++ b/resources/views/flux/icon/droplet.blade.php @@ -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 + +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" +> + + diff --git a/resources/views/livewire/actions/device-auto-join.blade.php b/resources/views/livewire/actions/device-auto-join.blade.php new file mode 100644 index 0000000..aa8f0f6 --- /dev/null +++ b/resources/views/livewire/actions/device-auto-join.blade.php @@ -0,0 +1,7 @@ +
+ @if($isFirstUser) + + + + @endif +
diff --git a/resources/views/livewire/auth/login.blade.php b/resources/views/livewire/auth/login.blade.php index 2a0387b..9934b1f 100644 --- a/resources/views/livewire/auth/login.blade.php +++ b/resources/views/livewire/auth/login.blade.php @@ -28,7 +28,7 @@ new #[Layout('components.layouts.auth')] class extends Component { $this->ensureIsNotRateLimited(); - if (! Auth::attempt(['email' => $this->email, 'password' => $this->password], $this->remember)) { + if (!Auth::attempt(['email' => $this->email, 'password' => $this->password], $this->remember)) { RateLimiter::hit($this->throttleKey()); throw ValidationException::withMessages([ @@ -47,7 +47,7 @@ new #[Layout('components.layouts.auth')] class extends Component { */ protected function ensureIsNotRateLimited(): void { - if (! RateLimiter::tooManyAttempts($this->throttleKey(), 5)) { + if (!RateLimiter::tooManyAttempts($this->throttleKey(), 5)) { return; } @@ -68,19 +68,20 @@ new #[Layout('components.layouts.auth')] class extends Component { */ protected function throttleKey(): string { - return Str::transliterate(Str::lower($this->email).'|'.request()->ip()); + return Str::transliterate(Str::lower($this->email) . '|' . request()->ip()); } }; ?>
- + - +
- +
@@ -102,15 +103,19 @@ new #[Layout('components.layouts.auth')] class extends Component {
- +
{{ __('Log in') }}
-
- Don't have an account? - Sign up -
+ + @if (Route::has('register')) +
+ Don't have an account? + Sign up +
+ @endif +
diff --git a/resources/views/livewire/device-dashboard.blade.php b/resources/views/livewire/device-dashboard.blade.php index 2b999c9..56d7244 100644 --- a/resources/views/livewire/device-dashboard.blade.php +++ b/resources/views/livewire/device-dashboard.blade.php @@ -1,3 +1,15 @@ + auth()->user()->devices()->paginate(10)]); + } +} +?> +
@if($devices->isEmpty()) @@ -21,7 +33,8 @@
@php $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

{{ $device->name }}

diff --git a/resources/views/livewire/device-manager.blade.php b/resources/views/livewire/device-manager.blade.php deleted file mode 100644 index 3668c94..0000000 --- a/resources/views/livewire/device-manager.blade.php +++ /dev/null @@ -1,124 +0,0 @@ -
- {{--@dump($devices)--}} -
-
-

Devices

- - Add Device - -
- @if (session()->has('message')) -
- {{ session('message') }} -
- @endif - - -
-
- Add Device -
- -
-
- -
- -
- -
- -
- -
- -
- -
- -
- -
-
- - Create Device -
- -
-
-
- - - - - - - - - - - - - - @foreach ($devices as $device) - - - - - - - - @endforeach - - - -
-
Name
-
-
Friendly ID
-
-
Mac Address
-
-
Refresh
-
-
Actions
-
- {{ $device->name }} - - {{ $device->friendly_id }} - -
- {{ $device->mac_address }} -
-
- {{ $device->default_refresh_interval }} - - - -
-
-
diff --git a/resources/views/livewire/devices/configure.blade.php b/resources/views/livewire/devices/configure.blade.php new file mode 100644 index 0000000..583b725 --- /dev/null +++ b/resources/views/livewire/devices/configure.blade.php @@ -0,0 +1,120 @@ +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'); + } +} +?> + +
+
+
+
+ @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 + +
+ +

{{ $device->name }}

+
+
+ + + + + + +
+
+ + {{$device->updated_at->diffForHumans()}} + + + + {{$device->mac_address}} + + + + {{$device->last_firmware_version}} + + + + + +
+
+ + + +
+
+ {{-- Edit TRMNL--}} + {{-- --}} +
+ + {{-- --}} + + + + +
+ + + {{-- Save changes--}} +
+
+
+ + +
+ Delete {{$device->name}}? +
+ +
+ + + + Cancel + + Delete device +
+
+ + + @if($current_image_uuid) + + Next Image + @endif +
+
+
+
+ diff --git a/resources/views/livewire/devices/manage.blade.php b/resources/views/livewire/devices/manage.blade.php new file mode 100644 index 0000000..e3935d0 --- /dev/null +++ b/resources/views/livewire/devices/manage.blade.php @@ -0,0 +1,205 @@ + '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(); + // } + } +} + +?> + +
+
+ {{--@dump($devices)--}} +
+
+

Devices

+ + Add Device + +
+ @if (session()->has('message')) +
+ {{ session('message') }} +
+ @endif + + +
+
+ Add Device +
+ +
+
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+
+ + Create Device +
+ +
+
+
+ + + + + + + + + + + + + + @foreach ($devices as $device) + + + + + + + + @endforeach + + + +
+
Name
+
+
Friendly ID
+
+
Mac Address
+
+
Refresh
+
+
Actions
+
+ {{ $device->name }} + + {{ $device->friendly_id }} + +
+ {{ $device->mac_address }} +
+
+ {{ $device->default_refresh_interval }} + +
+ + + + + + +
+
+
+
+ +
diff --git a/resources/views/livewire/plugins/api.blade.php b/resources/views/livewire/plugins/api.blade.php new file mode 100644 index 0000000..117e348 --- /dev/null +++ b/resources/views/livewire/plugins/api.blade.php @@ -0,0 +1,56 @@ +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; + } +}; +?> + +
+
+
+

API

+ +
+
+

+ POST + {{route('display.update')}} +

+
+

Headers

+
Authorization Bearer {{$token ?? '**********'}} + + Regenerate Token + +
+
+ +
+

Body

+
+
+{"markup":"<h1>Hello World</h1>"}
+                    
+
+
+
+
+
diff --git a/resources/views/livewire/plugins/index.blade.php b/resources/views/livewire/plugins/index.blade.php new file mode 100644 index 0000000..562ef30 --- /dev/null +++ b/resources/views/livewire/plugins/index.blade.php @@ -0,0 +1,36 @@ + + ['name' => 'Markup', 'icon' => 'code-backet', 'route' => 'plugins.markup'], + 'api' => + ['name' => 'API', 'icon' => 'code-backet', 'route' => 'plugins.api'], + ]; + +}; +?> + +
+
+
+

Plugins

+
+
+ @foreach($plugins as $plugin) + + @endforeach +
+
+
diff --git a/resources/views/livewire/plugins/markup.blade.php b/resources/views/livewire/plugins/markup.blade.php new file mode 100644 index 0000000..7b6d300 --- /dev/null +++ b/resources/views/livewire/plugins/markup.blade.php @@ -0,0 +1,171 @@ +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 = '

Hello World!

'; + break; + } + $this->blade_code = $markup; + } + + public function renderQuote(): string + { + return << + + + Motivational Quote + “I love inside jokes. I hope to be a part of one someday.” + Michael Scott + + + + +HTML; + } + + public function renderTrainMonitor() + { + return << + + + + + Abfahrt + Aktuell + Zug + Ziel + Steig + + + + + 08:51 + 08:52 + REX 1 + Vienna Main Station + 3 + + + + + + +HTML; + + } + + public function renderHomeAssistant() + { + return << + + + + + + + 23.3° + + + 47.52 % + + Sensor 1 + + + + + + + +HTML; + + } + + +}; +?> + +
+
+

Markup

+ + {{--
--}} + +
+ Examples + +
+
+
+ +
+ +
+ + + Generate Screen + +
+
+ + {{--
--}} +
+
diff --git a/routes/api.php b/routes/api.php index b44e491..d89d483 100644 --- a/routes/api.php +++ b/routes/api.php @@ -1,21 +1,39 @@ 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' => 'MAC Address not registered or invalid access token', - ], 404); + // 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([ + 'message' => 'MAC Address not registered or invalid access token', + ], 404); + } } $device->update([ @@ -25,9 +43,13 @@ Route::get('/display', function (Request $request) { ]); $image_uuid = $device->current_screen_image; - - $image_path = 'images/generated/'.$image_uuid.'.bmp'; - $filename = basename($image_path); + if (! $image_uuid) { + $image_path = 'images/setup-logo.bmp'; + $filename = 'setup-logo.bmp'; + } else { + $image_path = 'images/generated/'.$image_uuid.'.bmp'; + $filename = basename($image_path); + } return response()->json([ 'status' => '0', @@ -67,8 +89,24 @@ Route::get('/setup', 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) { \Log::info('Device Log', $log); } @@ -81,3 +119,23 @@ Route::post('/log', function (Request $request) { Route::get('/user', function (Request $request) { return $request->user(); })->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'); diff --git a/routes/auth.php b/routes/auth.php index 62e6352..5647405 100644 --- a/routes/auth.php +++ b/routes/auth.php @@ -8,8 +8,10 @@ Route::middleware('guest')->group(function () { Volt::route('login', 'auth.login') ->name('login'); - Volt::route('register', 'auth.register') - ->name('register'); + if (config('app.registration.enabled')) { + Volt::route('register', 'auth.register') + ->name('register'); + } Volt::route('forgot-password', 'auth.forgot-password') ->name('password.request'); diff --git a/routes/console.php b/routes/console.php index aaf16f7..b4cff25 100644 --- a/routes/console.php +++ b/routes/console.php @@ -1,8 +1,9 @@ comment(Inspiring::quote()); -//})->purpose('Display an inspiring quote')->hourly(); +// })->purpose('Display an inspiring quote')->hourly(); + +Schedule::job(new FetchProxyCloudResponses)->everyFifteenMinutes(); diff --git a/routes/web.php b/routes/web.php index e81cf05..355b892 100644 --- a/routes/web.php +++ b/routes/web.php @@ -7,30 +7,21 @@ Route::get('/', function () { return view('welcome'); })->name('home'); -Route::view('dashboard', 'dashboard') - ->middleware(['auth', 'verified']) - ->name('dashboard'); - Route::middleware(['auth'])->group(function () { Route::redirect('settings', 'settings/profile'); - Volt::route('settings/profile', 'settings.profile')->name('settings.profile'); Volt::route('settings/password', 'settings.password')->name('settings.password'); Volt::route('settings/appearance', 'settings.appearance')->name('settings.appearance'); - Route::get('/devices', function () { - return view('devices'); - })->name('devices'); + Volt::route('/dashboard', 'device-dashboard')->name('dashboard'); - Route::get('/devices/{device}/configure', function (App\Models\Device $device) { - $current_image_uuid = auth()->user()->devices()->find($device->id)->current_screen_image; - $current_image_path = 'images/generated/' . $current_image_uuid . '.png'; + Volt::route('/devices', 'devices.manage')->name('devices'); + Volt::route('/devices/{device}/configure', 'devices.configure')->name('devices.configure'); - return view('devices.configure', compact('device'), [ - 'image' => ($current_image_uuid) ? url($current_image_path) : null, - ]); - })->name('devices.configure'); + Volt::route('plugins', 'plugins.index')->name('plugins.index'); + 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'; diff --git a/tests/Feature/Auth/AuthenticationTest.php b/tests/Feature/Auth/AuthenticationTest.php index 10df8e5..61d04f1 100644 --- a/tests/Feature/Auth/AuthenticationTest.php +++ b/tests/Feature/Auth/AuthenticationTest.php @@ -44,4 +44,4 @@ test('users can logout', function () { $this->assertGuest(); $response->assertRedirect('/'); -}); \ No newline at end of file +}); diff --git a/tests/Feature/Auth/EmailVerificationTest.php b/tests/Feature/Auth/EmailVerificationTest.php index 2880c6b..37a205f 100644 --- a/tests/Feature/Auth/EmailVerificationTest.php +++ b/tests/Feature/Auth/EmailVerificationTest.php @@ -46,4 +46,4 @@ test('email is not verified with invalid hash', function () { $this->actingAs($user)->get($verificationUrl); expect($user->fresh()->hasVerifiedEmail())->toBeFalse(); -}); \ No newline at end of file +}); diff --git a/tests/Feature/Auth/PasswordConfirmationTest.php b/tests/Feature/Auth/PasswordConfirmationTest.php index ba015f6..3f9b423 100644 --- a/tests/Feature/Auth/PasswordConfirmationTest.php +++ b/tests/Feature/Auth/PasswordConfirmationTest.php @@ -37,4 +37,4 @@ test('password is not confirmed with invalid password', function () { ->call('confirmPassword'); $response->assertHasErrors(['password']); -}); \ No newline at end of file +}); diff --git a/tests/Feature/Auth/PasswordResetTest.php b/tests/Feature/Auth/PasswordResetTest.php index 859312d..b678d73 100644 --- a/tests/Feature/Auth/PasswordResetTest.php +++ b/tests/Feature/Auth/PasswordResetTest.php @@ -65,4 +65,4 @@ test('password can be reset with valid token', function () { return true; }); -}); \ No newline at end of file +}); diff --git a/tests/Feature/Auth/RegistrationTest.php b/tests/Feature/Auth/RegistrationTest.php index 399e6a6..1ef6256 100644 --- a/tests/Feature/Auth/RegistrationTest.php +++ b/tests/Feature/Auth/RegistrationTest.php @@ -23,4 +23,4 @@ test('new users can register', function () { ->assertRedirect(route('dashboard', absolute: false)); $this->assertAuthenticated(); -}); \ No newline at end of file +}); diff --git a/tests/Feature/DashboardTest.php b/tests/Feature/DashboardTest.php index 26d1d03..e11099a 100644 --- a/tests/Feature/DashboardTest.php +++ b/tests/Feature/DashboardTest.php @@ -15,4 +15,4 @@ test('authenticated users can visit the dashboard', function () { $response = $this->get('/dashboard'); $response->assertStatus(200); -}); \ No newline at end of file +}); diff --git a/tests/Feature/Settings/PasswordUpdateTest.php b/tests/Feature/Settings/PasswordUpdateTest.php index 5cc1209..d0c32c5 100644 --- a/tests/Feature/Settings/PasswordUpdateTest.php +++ b/tests/Feature/Settings/PasswordUpdateTest.php @@ -38,4 +38,4 @@ test('correct password must be provided to update password', function () { ->call('updatePassword'); $response->assertHasErrors(['current_password']); -}); \ No newline at end of file +}); diff --git a/tests/Feature/Settings/ProfileUpdateTest.php b/tests/Feature/Settings/ProfileUpdateTest.php index d876e5d..6628ccc 100644 --- a/tests/Feature/Settings/ProfileUpdateTest.php +++ b/tests/Feature/Settings/ProfileUpdateTest.php @@ -74,4 +74,4 @@ test('correct password must be provided to delete account', function () { $response->assertHasErrors(['password']); expect($user->fresh())->not->toBeNull(); -}); \ No newline at end of file +});