Remove Amazon filament price tracking
Deploy HickLab3D Dashboard / Deploy to production (push) Successful in 2m41s

This commit is contained in:
Hickmeister
2026-10-11 15:56:13 +01:00
parent 1c72937f9a
commit 96561dc3d2
20 changed files with 54 additions and 350 deletions
@@ -1,67 +0,0 @@
<?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;
}
}
+24 -20
View File
@@ -3,7 +3,6 @@
namespace App\Http\Controllers;
use App\Models\EtsyConnection;
use App\Models\Filament;
use App\Services\Bambuddy;
use App\Services\DryerClient;
use App\Services\Etsy;
@@ -18,16 +17,6 @@ class DashboardController extends Controller
public function __invoke(Request $request, Etsy $etsy, Bambuddy $bambuddy, Parcel2Go $parcel2Go, DryerClient $dryer): View
{
$isAdmin = $request->user()->role === 'admin';
$filamentCount = Filament::count();
$recentFilaments = Filament::with(['prices' => fn ($query) => $query->orderByDesc('recordedAt')->limit(1)])
->orderByDesc('id')->limit(4)->get();
$materials = Filament::query()->select('material')->selectRaw('COUNT(*) as total')
->groupBy('material')->orderByDesc('total')->get()
->map(fn (Filament $filament): array => [
'label' => $filament->material ?: 'Unspecified',
'count' => (int) $filament->total,
])->all();
$etsyStatus = $isAdmin ? 'Not connected' : null;
$etsyConnected = false;
$receipts = [];
@@ -75,11 +64,25 @@ class DashboardController extends Controller
}
}
$printingCount = collect($printers)->filter(fn (array $printer): bool => in_array(
strtoupper((string) ($printer['live']['state'] ?? $printer['status'] ?? '')),
['RUNNING', 'PRINTING'],
true,
))->count();
$printerStates = collect($printers)->countBy(function (array $printer): string {
$live = $printer['live'] ?? [];
$state = strtoupper((string) ($live['state'] ?? $printer['status'] ?? ''));
if ($live === []) {
return 'Unavailable';
}
if (($live['connected'] ?? false) === false) {
return 'Offline';
}
if (in_array($state, ['RUNNING', 'PRINTING'], true)) {
return 'Printing';
}
if (in_array($state, ['IDLE', 'READY', 'FINISH'], true)) {
return 'Idle';
}
return 'Other';
});
$dryerState = 'Not configured';
if (filled(config('services.dryer.api_key'))) {
@@ -99,9 +102,6 @@ class DashboardController extends Controller
return view('dashboard', [
'isAdmin' => $isAdmin,
'filamentCount' => $filamentCount,
'recentFilaments' => $recentFilaments,
'materials' => $materials,
'receipts' => array_slice($receipts, 0, 5),
'awaitingDispatch' => count($receipts),
'listingCount' => $listingCount,
@@ -111,7 +111,11 @@ class DashboardController extends Controller
'shippingStatus' => $shippingStatus,
'printers' => $printers,
'printerStatus' => $printerStatus,
'printingCount' => $printingCount,
'printingCount' => $printerStates->get('Printing', 0),
'printerChart' => $printerStates->map(fn (int $count, string $state): array => [
'label' => $state,
'count' => $count,
])->values()->all(),
'dryerState' => $dryerState,
'orderChart' => [
'labels' => $days->map(fn (Carbon $day): string => $day->format('d M'))->all(),
@@ -1,52 +0,0 @@
<?php
namespace App\Http\Controllers;
use App\Models\Filament;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\View\View;
class FilamentController extends Controller
{
public function index(): View
{
return view('filaments.index', ['filaments' => Filament::with(['prices' => fn ($query) => $query->orderByDesc('recordedAt')->limit(10)])->orderBy('filamentName')->paginate(20)]);
}
public function create(Request $request): View
{
abort_unless($request->user()->role === 'admin', 403);
return view('filaments.create');
}
public function store(Request $request): RedirectResponse
{
abort_unless($request->user()->role === 'admin', 403);
$data = $request->validate([
'filamentName' => ['required', 'string', 'max:255'],
'amazonUrl' => ['required', 'url:http,https', 'max:2048'],
'brand' => ['nullable', 'string', 'max:255'],
'material' => ['nullable', 'string', 'max:255'],
'color' => ['nullable', 'string', 'max:255'],
'filamentWeight' => ['nullable', 'numeric', 'min:0'],
'itemDiameter' => ['nullable', 'numeric', 'min:0'],
]);
$host = strtolower((string) parse_url($data['amazonUrl'], PHP_URL_HOST));
if (! in_array($host, ['amazon.co.uk', 'www.amazon.co.uk', 'amazon.com', 'www.amazon.com'], true)) {
return back()->withErrors(['amazonUrl' => 'Enter an Amazon UK or US product URL.'])->withInput();
}
Filament::create([...$data, 'userId' => $request->user()->id]);
return redirect()->route('filaments.index')->with('status', 'Filament added. Price collection runs on the schedule.');
}
public function destroy(Request $request, Filament $filament): RedirectResponse
{
abort_unless($request->user()->role === 'admin', 403);
$filament->delete();
return redirect()->route('filaments.index')->with('status', 'Filament removed.');
}
}
-2
View File
@@ -25,7 +25,6 @@ class UserController extends Controller
'email' => ['required', 'email', 'max:255', 'unique:users,email'],
'role' => ['required', Rule::in(['admin', 'user'])],
'password' => ['required', 'string', 'min:12'],
'alertEmails' => ['sometimes', 'boolean'],
]);
User::create($data);
@@ -39,7 +38,6 @@ class UserController extends Controller
'email' => ['sometimes', 'email', Rule::unique('users', 'email')->ignore($user->id)],
'role' => ['sometimes', Rule::in(['admin', 'user'])],
'disabled' => ['sometimes', 'boolean'],
'alertEmails' => ['sometimes', 'boolean'],
'password' => ['nullable', 'string', 'min:12'],
]);
if (empty($data['password'])) {
-25
View File
@@ -1,25 +0,0 @@
<?php
namespace App\Models;
use Database\Factories\FilamentFactory;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
class Filament extends Model
{
/** @use HasFactory<FilamentFactory> */
use HasFactory;
protected $table = 'filamentTracker';
public $timestamps = false;
protected $fillable = ['userId', 'filamentName', 'amazonUrl', 'filamentWeight', 'brand', 'material', 'color', 'itemDiameter'];
public function prices(): HasMany
{
return $this->hasMany(FilamentPrice::class, 'filamentId');
}
}
-24
View File
@@ -1,24 +0,0 @@
<?php
namespace App\Models;
use Database\Factories\FilamentPriceFactory;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class FilamentPrice extends Model
{
/** @use HasFactory<FilamentPriceFactory> */
use HasFactory;
protected $table = 'filamentPriceHistory';
public $timestamps = false;
protected $fillable = ['filamentId', 'price', 'currentDiscount'];
protected function casts(): array
{
return ['currentDiscount' => 'array', 'recordedAt' => 'datetime', 'price' => 'decimal:2'];
}
}
+1 -2
View File
@@ -10,7 +10,7 @@ use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
#[Fillable(['username', 'email', 'role', 'disabled', 'alertEmails', 'password'])]
#[Fillable(['username', 'email', 'role', 'disabled', 'password'])]
#[Hidden(['password'])]
class User extends Authenticatable
{
@@ -29,7 +29,6 @@ class User extends Authenticatable
return [
'password' => 'hashed',
'disabled' => 'boolean',
'alertEmails' => 'boolean',
];
}
}