const DEFAULT_PROBE_NAMES = ['Probe 1', 'Probe 2', 'Probe 3', 'Probe 4']; const PRESETS = [ { label: 'Brisket', temp: 203 }, { label: 'Pork Butt', temp: 205 }, { label: 'Poultry (breast)', temp: 165 }, { label: 'Poultry (thigh)', temp: 175 }, { label: 'Ribs', temp: 203 }, ]; let activeSession = null; let latestReading = null; let setpoints = []; let chartInstance = null; async function api(path, options = {}) { const resp = await fetch(path, options); if (!resp.ok) throw new Error(`${resp.status} ${resp.statusText}`); return resp.json(); } function formatTime(iso) { if (!iso) return '--'; const d = new Date(iso + (iso.endsWith('Z') ? '' : 'Z')); return d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' }); } function statusClass(connected) { return connected ? 'ok' : 'danger'; } function renderProbeCards(reading, setpoints) { const container = document.getElementById('probe-cards'); container.innerHTML = ''; const probes = reading?.probes || []; const unit = reading?.unit || 'F'; for (let i = 0; i < 4; i++) { const probe = probes[i] || { id: i + 1, name: DEFAULT_PROBE_NAMES[i], temperature: null, connected: false }; const setpoint = setpoints.find(s => s.probe_number === (i + 1)); const card = document.createElement('div'); card.className = `probe-card ${probe.connected ? 'connected' : ''}`; const tempDisplay = probe.connected ? `
${probe.temperature.toFixed(1)}°${unit}
` : `
---
`; const targetDisplay = setpoint ? `
Target: ${setpoint.label || ''} ${setpoint.target_temp.toFixed(0)}°${unit}
` : `
No target set
`; card.innerHTML = `
${probe.name || DEFAULT_PROBE_NAMES[i]} #${i + 1}
${targetDisplay} ${tempDisplay}
`; container.appendChild(card); } // Wire up preset selectors and save buttons. document.querySelectorAll('.preset-select').forEach(sel => { sel.addEventListener('change', () => { const probe = sel.dataset.probe; const customInput = document.querySelector(`.custom-target[data-probe="${probe}"]`); if (sel.value && sel.value !== 'custom') { customInput.value = sel.value; } }); }); document.querySelectorAll('.save-setpoint').forEach(btn => { btn.addEventListener('click', async () => { const probe = parseInt(btn.dataset.probe, 10); const target = parseFloat(document.querySelector(`.custom-target[data-probe="${probe}"]`).value); const label = document.querySelector(`.target-label[data-probe="${probe}"]`).value; if (isNaN(target)) return; await saveSetpoint(probe, label, target); }); }); } async function saveSetpoint(probeNumber, label, targetTemp) { if (!activeSession) return; await api(`/api/setpoints/${activeSession.id}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ probe_number: probeNumber, label, target_temp: targetTemp }), }); await loadSetpoints(); await refreshDashboard(); } function renderStatusBar(reading) { const cydEl = document.getElementById('cyd-status'); const battEl = document.getElementById('battery-status'); const sigEl = document.getElementById('signal-status'); if (!reading) { cydEl.textContent = 'CYD unreachable'; cydEl.className = 'danger'; return; } const connected = reading.connected; cydEl.textContent = connected ? 'CYD connected' : 'CYD: BLE disconnected'; cydEl.className = connected ? 'ok' : 'warn'; battEl.textContent = `Battery: ${reading.battery ?? '--'}%`; const rssi = reading.rssi; sigEl.textContent = rssi !== undefined ? `Signal: ${rssi} dBm` : 'Signal: WiFi only'; } async function loadSessions() { const sessions = await api('/api/sessions'); activeSession = sessions.find(s => !s.ended_at) || null; const activeEl = document.getElementById('active-session'); const endBtn = document.getElementById('end-session-btn'); if (activeSession) { activeEl.innerHTML = ` ${activeSession.name} started at ${formatTime(activeSession.started_at)}
${activeSession.notes || 'No notes'} `; endBtn.disabled = false; } else { activeEl.textContent = 'No active session.'; endBtn.disabled = true; } } async function loadSetpoints() { if (!activeSession) { setpoints = []; return; } setpoints = await api(`/api/setpoints/${activeSession.id}`); } async function refreshDashboard() { try { const status = await api('/api/status'); latestReading = status.latest_reading; // Add probe names from active session if we had stored them; for now use CYD names. if (latestReading) { // The CYD returns probe names; pass them through. } renderStatusBar(status.latest_reading); renderProbeCards(status.latest_reading, setpoints); document.getElementById('last-updated').textContent = `Last updated: ${formatTime(new Date().toISOString())}`; } catch (e) { console.error('Refresh failed:', e); renderStatusBar(null); } } async function refreshChart() { if (!activeSession || !chartInstance) return; const range = parseInt(document.getElementById('chart-range').value, 10); const data = await api(`/api/readings/${activeSession.id}?minutes=${range}`); const labels = data.map(r => formatTime(r.received_at)); const datasets = [1, 2, 3, 4].map((num, idx) => ({ label: (latestReading?.probes?.[idx]?.name) || `Probe ${num}`, data: data.map(r => r[`probe${num}_temp`]), borderColor: ['#ff6b35', '#4caf50', '#2196f3', '#9c27b0'][idx], backgroundColor: ['rgba(255,107,53,0.1)', 'rgba(76,175,80,0.1)', 'rgba(33,150,243,0.1)', 'rgba(156,39,176,0.1)'][idx], fill: true, tension: 0.3, spanGaps: true, })); chartInstance.data.labels = labels; chartInstance.data.datasets = datasets; chartInstance.update(); } function initChart() { const ctx = document.getElementById('temp-chart').getContext('2d'); chartInstance = new Chart(ctx, { type: 'line', data: { labels: [], datasets: [] }, options: { responsive: true, maintainAspectRatio: false, scales: { y: { title: { display: true, text: 'Temperature' }, grid: { color: '#333' }, ticks: { color: '#aaa' }, }, x: { grid: { color: '#333' }, ticks: { color: '#aaa', maxTicksLimit: 8 }, }, }, plugins: { legend: { labels: { color: '#f0f0f0' } }, }, }, }); } document.getElementById('new-session-form').addEventListener('submit', async (e) => { e.preventDefault(); const name = document.getElementById('session-name').value; const notes = document.getElementById('session-notes').value; await api('/api/sessions', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name, notes }), }); document.getElementById('session-name').value = ''; document.getElementById('session-notes').value = ''; await loadSessions(); await loadSetpoints(); await refreshChart(); }); document.getElementById('end-session-btn').addEventListener('click', async () => { if (!activeSession) return; await api(`/api/sessions/${activeSession.id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ end: true }), }); await loadSessions(); await loadSetpoints(); await refreshChart(); }); document.getElementById('refresh-chart-btn').addEventListener('click', refreshChart); document.getElementById('chart-range').addEventListener('change', refreshChart); async function init() { initChart(); await loadSessions(); await loadSetpoints(); await refreshDashboard(); await refreshChart(); setInterval(async () => { await refreshDashboard(); }, 5000); setInterval(async () => { if (activeSession) await refreshChart(); }, 30000); } init();