Files
Hickmeister ff9960fe06
Deploy HickLab3D Dashboard / Deploy to production (push) Successful in 1m42s
Move integration credentials into encrypted dashboard settings
2026-10-11 16:15:02 +01:00

64 lines
8.2 KiB
PHP

@extends('layouts.app')
@section('title', 'Filament dryer')
@section('content')
<div class="flex flex-wrap items-end justify-between gap-4"><div><p class="text-xs font-bold uppercase tracking-[.2em] text-brand">Workshop tools</p><h2 class="mt-2 text-2xl font-bold">Filament dryer</h2><p class="mt-1 text-slate-400">Live programme, sensor readings and relay state.</p></div>@if($dryerUrl)<a href="{{ $dryerUrl }}" target="_blank" rel="noopener noreferrer" class="rounded-xl border border-brand/40 px-4 py-3 text-sm font-semibold text-brand hover:bg-brand/10">Open dryer ↗</a>@endif</div>
<p id="dryer-message" role="status" class="rounded-xl border border-slate-700 bg-slate-900 px-4 py-3 text-sm text-slate-400">Connecting to dryer…</p>
<div id="dryer-fault" role="alert" hidden class="rounded-xl border border-rose-500/40 bg-rose-500/10 p-4 text-sm text-rose-200"><strong>Dryer fault</strong><p id="fault-detail" class="mt-1"></p></div>
<div class="grid gap-4 sm:grid-cols-2 xl:grid-cols-4">@foreach([['dryer-state','State'],['temperature','Temperature'],['humidity','Humidity'],['remaining','Remaining time']] as [$id,$label])<div class="rounded-2xl border border-slate-800 bg-slate-900 p-6"><p class="text-sm text-slate-400">{{ $label }}</p><p id="{{ $id }}" class="mt-5 text-3xl font-bold">—</p></div>@endforeach</div>
<div class="grid gap-4 lg:grid-cols-2"><section class="rounded-2xl border border-slate-800 bg-slate-900 p-6"><h3 class="text-lg font-semibold">Current programme</h3><dl class="mt-5 grid grid-cols-2 gap-4 text-sm"><div><dt class="text-slate-400">Profile</dt><dd id="profile" class="mt-1 font-semibold">—</dd></div><div><dt class="text-slate-400">Target</dt><dd id="target" class="mt-1 font-semibold">—</dd></div><div><dt class="text-slate-400">Heater</dt><dd id="heater" class="mt-1 font-semibold">—</dd></div><div><dt class="text-slate-400">Fan</dt><dd id="fan" class="mt-1 font-semibold">—</dd></div></dl></section>
<section class="rounded-2xl border border-slate-800 bg-slate-900 p-6"><h3 class="text-lg font-semibold">Recent samples</h3><p class="mt-1 text-sm text-slate-400">Latest readings from the controller.</p><div id="dryer-history" class="mt-4 flex flex-wrap gap-2 text-xs text-slate-400">Waiting for history…</div></section></div>
@if(auth()->user()->role === 'admin')<form id="dryer-form" class="grid gap-5 rounded-2xl border border-slate-800 bg-slate-900 p-6"><div><h3 class="text-lg font-semibold">Start a drying cycle</h3><p class="mt-1 text-sm text-slate-400">The controller checks temperature, timing and fault limits before heating.</p></div><div class="grid gap-4 sm:grid-cols-3"><label class="grid gap-2 text-sm">Material<select id="preset" class="rounded-xl border border-slate-700 bg-slate-950 px-4 py-3"><option value="PLA" data-temp="50">PLA · 50°C</option><option value="PETG" data-temp="60">PETG · 60°C</option><option value="ABS" data-temp="65">ABS · 65°C</option><option value="Nylon" data-temp="65">Nylon · 65°C</option><option value="Custom" data-temp="50">Custom</option></select></label><label class="grid gap-2 text-sm">Target °C<input id="target-temperature" type="number" min="45" max="70" value="50" required class="rounded-xl border border-slate-700 bg-slate-950 px-4 py-3"></label><label class="grid gap-2 text-sm">Duration, minutes<input id="duration" type="number" min="5" max="2880" value="240" required class="rounded-xl border border-slate-700 bg-slate-950 px-4 py-3"></label></div><div class="flex flex-wrap gap-3"><button id="start-dryer" class="rounded-xl bg-brand px-5 py-3 font-semibold text-slate-950 hover:bg-brand/90">Start cycle</button><button type="button" id="stop-dryer" class="rounded-xl border border-rose-500/40 px-5 py-3 text-rose-300 hover:bg-rose-500/10">Stop cycle</button><button type="button" id="acknowledge-fault" hidden class="rounded-xl border border-amber-500/40 px-5 py-3 text-amber-200 hover:bg-amber-500/10">Acknowledge fault</button></div></form>@endif
@push('scripts')<script>
const statusUrl = @json(route('dryer.status'));
const controlUrl = @json(route('dryer.control'));
const acknowledgeUrl = @json(route('dryer.acknowledge'));
const message = document.getElementById('dryer-message');
const display = (id, value) => { document.getElementById(id).textContent = value; };
async function refreshDryer() {
try {
const response = await fetch(statusUrl, {credentials: 'same-origin', cache: 'no-store'});
if (!response.ok) throw new Error('Dryer is unavailable. Check its connection and API key.');
const data = await response.json();
const status = data.status || {};
const sample = status.sample || {};
const programme = status.programme || {};
const outputs = status.outputs || {};
const fault = status.fault || {};
display('dryer-state', status.state || 'Unknown');
display('temperature', sample.valid && sample.temperatureC != null ? Number(sample.temperatureC).toFixed(1) + '°C' : 'Sensor unavailable');
display('humidity', sample.valid && sample.humidityPct != null ? Number(sample.humidityPct).toFixed(1) + '%' : '—');
display('remaining', programme.remainingSeconds != null ? Math.ceil(programme.remainingSeconds / 60) + ' min' : '—');
display('profile', status.profile || '—');
display('target', programme.targetC ? Number(programme.targetC).toFixed(0) + '°C' : '—');
display('heater', outputs.heater ? 'On' : 'Off');
display('fan', outputs.fan ? 'On' : 'Off');
document.getElementById('dryer-fault').hidden = !fault.latched;
document.getElementById('fault-detail').textContent = fault.message || fault.code || 'Controller reported a fault.';
const acknowledge = document.getElementById('acknowledge-fault');
if (acknowledge) acknowledge.hidden = !fault.latched;
const samples = Array.isArray(data.history?.samples) ? data.history.samples.slice(-8) : [];
const history = document.getElementById('dryer-history');
history.replaceChildren();
if (!samples.length) history.textContent = 'No samples yet.';
for (const sample of samples) {
const item = document.createElement('span');
item.className = 'rounded-lg bg-slate-950 px-3 py-2';
item.textContent = Number(sample.temperatureC).toFixed(1) + '°C · ' + Number(sample.humidityPct).toFixed(0) + '%';
history.append(item);
}
message.textContent = 'Controller online · updated ' + new Date().toLocaleTimeString();
} catch (error) { message.textContent = error.message; }
}
async function sendDryerCommand(url, body) {
const response = await fetch(url, {method: 'POST', credentials: 'same-origin', headers: {'Content-Type': 'application/json', 'Accept': 'application/json', 'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content}, body: JSON.stringify(body)});
if (!response.ok) throw new Error('The controller did not accept that command.');
await refreshDryer();
}
document.getElementById('preset')?.addEventListener('change', event => {document.getElementById('target-temperature').value = event.target.selectedOptions[0].dataset.temp;});
document.getElementById('dryer-form')?.addEventListener('submit', async event => {event.preventDefault(); const button = document.getElementById('start-dryer'); button.disabled = true; try {await sendDryerCommand(controlUrl, {dryerOn: true, profile: document.getElementById('preset').value, targetTemperature: Number(document.getElementById('target-temperature').value), dryingTime: Number(document.getElementById('duration').value)})} catch (error) {message.textContent = error.message} finally {button.disabled = false}});
document.getElementById('stop-dryer')?.addEventListener('click', async () => {if (confirm('Stop the current drying cycle?')) {try {await sendDryerCommand(controlUrl, {dryerOn: false})} catch (error) {message.textContent = error.message}}});
document.getElementById('acknowledge-fault')?.addEventListener('click', async () => {if (confirm('Acknowledge the dryer fault? Only do this after checking the appliance.')) {try {await sendDryerCommand(acknowledgeUrl, {})} catch (error) {message.textContent = error.message}}});
refreshDryer(); setInterval(refreshDryer, 10000);
</script>@endpush
@endsection