Migrate HickLab3D dashboard and add production auto deployment
Deploy HickLab3D Dashboard / Deploy to production (push) Failing after 19s

This commit is contained in:
Hickmeister
2026-10-10 18:51:23 +01:00
parent 8410a50f26
commit 88dd00551f
974 changed files with 14609 additions and 162573 deletions
@@ -0,0 +1,53 @@
<?php
namespace App\Console\Commands;
use App\Models\Printer;
use App\Models\PrinterTelemetry;
use Illuminate\Console\Attributes\Description;
use Illuminate\Console\Attributes\Signature;
use Illuminate\Console\Command;
use PhpMqtt\Client\ConnectionSettings;
use PhpMqtt\Client\MqttClient;
use Throwable;
#[Signature('printers:collect')]
#[Description('Collect a short telemetry sample from each registered Bambu printer')]
class CollectPrinterTelemetry extends Command
{
public function handle(): int
{
foreach (Printer::cursor() as $printer) {
$client = new MqttClient($printer->printerIp, config('services.mqtt.port'), 'hicklab3d_'.bin2hex(random_bytes(6)));
try {
$settings = (new ConnectionSettings)
->setUsername(config('services.mqtt.username'))
->setPassword($printer->mqttPassword)
->setConnectTimeout(5)
->setUseTls(true)
->setTlsVerifyPeer(config('services.mqtt.verify_tls'))
->setTlsVerifyPeerName(config('services.mqtt.verify_tls'));
$client->connect($settings, true);
$received = false;
$client->subscribe('device/'.$printer->serialNumber.'/report', function (string $topic, string $message) use ($printer, &$received): void {
$telemetry = json_decode($message, true);
if (is_array($telemetry)) {
PrinterTelemetry::create(['printerId' => $printer->id, 'telemetry' => $telemetry]);
$received = true;
}
});
$started = microtime(true);
while (! $received && microtime(true) - $started < 15) {
$client->loopOnce($started, true);
}
$client->disconnect();
$this->line($printer->printerName.($received ? ': received telemetry' : ': no telemetry received'));
} catch (Throwable $exception) {
report($exception);
$this->warn($printer->printerName.': connection failed');
}
}
return self::SUCCESS;
}
}
+34
View File
@@ -0,0 +1,34 @@
<?php
namespace App\Console\Commands;
use App\Models\User;
use Illuminate\Console\Attributes\Description;
use Illuminate\Console\Attributes\Signature;
use Illuminate\Console\Command;
#[Signature('app:create-admin {username} {email}')]
#[Description('Create the first HickLab3D administrator')]
class CreateAdmin extends Command
{
public function handle(): int
{
$username = (string) $this->argument('username');
$email = (string) $this->argument('email');
if (User::where('username', $username)->orWhere('email', $email)->exists()) {
$this->error('That username or email already exists.');
return self::FAILURE;
}
$password = (string) $this->secret('Password (at least 12 characters)');
if (mb_strlen($password) < 12) {
$this->error('Password is too short.');
return self::FAILURE;
}
User::create(['username' => $username, 'email' => $email, 'role' => 'admin', 'password' => $password]);
$this->info('Administrator created.');
return self::SUCCESS;
}
}
@@ -0,0 +1,67 @@
<?php
namespace App\Console\Commands;
use App\Models\Filament;
use App\Models\FilamentPrice;
use App\Models\User;
use Illuminate\Console\Attributes\Description;
use Illuminate\Console\Attributes\Signature;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Mail;
use Throwable;
#[Signature('filaments:refresh')]
#[Description('Collect current Amazon prices for tracked filament')]
class RefreshFilamentPrices extends Command
{
public function handle(): int
{
$drops = [];
foreach (Filament::cursor() as $filament) {
$host = strtolower((string) parse_url($filament->amazonUrl, PHP_URL_HOST));
if (! in_array($host, ['amazon.co.uk', 'www.amazon.co.uk', 'amazon.com', 'www.amazon.com'], true)) {
$this->warn($filament->filamentName.': unsupported product host');
continue;
}
try {
$html = Http::timeout(15)->withHeaders(['User-Agent' => 'Mozilla/5.0 HickLab3D/1.0'])
->get($filament->amazonUrl)->throw()->body();
if (! preg_match('/class="[^"]*a-price-whole[^"]*"[^>]*>([0-9,.]+)/', $html, $whole)
|| ! preg_match('/class="[^"]*a-price-fraction[^"]*"[^>]*>([0-9]{2})/', $html, $fraction)) {
$this->warn($filament->filamentName.': price unavailable');
continue;
}
$price = (float) (str_replace(',', '', $whole[1]).'.'.$fraction[1]);
if ($price <= 0) {
continue;
}
preg_match('/class="[^"]*savingsPercentage[^"]*"[^>]*>[^0-9]*([0-9]+)%/', $html, $discount);
$previous = FilamentPrice::where('filamentId', $filament->id)->orderByDesc('recordedAt')->orderByDesc('id')->first();
FilamentPrice::create([
'filamentId' => $filament->id,
'price' => $price,
'currentDiscount' => ['discount' => ['value' => (int) ($discount[1] ?? 0), 'type' => 'percentage'], 'voucher' => ['value' => 0, 'type' => 'none']],
]);
if ($previous && $price < (float) $previous->price) {
$drops[] = $filament->filamentName.': £'.number_format((float) $previous->price, 2).' → £'.number_format($price, 2).' ('.$filament->amazonUrl.')';
}
$this->line($filament->filamentName.': £'.number_format($price, 2));
} catch (Throwable $exception) {
report($exception);
$this->warn($filament->filamentName.': collection failed');
}
}
if ($drops) {
$body = "HickLab3D filament price drops\n\n".implode("\n", $drops);
foreach (User::where('alertEmails', true)->where('disabled', false)->pluck('email') as $email) {
Mail::raw($body, fn ($message) => $message->to($email)->subject('HickLab3D filament price drops'));
}
}
return self::SUCCESS;
}
}