diff --git a/frontend/app.py b/frontend/app.py index 9a82fa5..7fc532d 100644 --- a/frontend/app.py +++ b/frontend/app.py @@ -84,6 +84,51 @@ def api_cyd_latest(): return jsonify(payload) +def _fetch_cyd_settings(): + try: + resp = requests.get(f"{config.CYD_BASE_URL}/api/settings", timeout=10) + resp.raise_for_status() + return resp.json() + except Exception as e: + app.logger.warning(f"CYD settings fetch failed: {e}") + return None + + +@app.route("/api/probes", methods=["GET", "PUT"]) +def probes(): + if request.method == "GET": + settings = _fetch_cyd_settings() + if settings is None: + # Fall back to the latest reading so the UI still has something to show. + latest = fetch_cyd() + names = [] + if latest: + names = [p.get("name", f"Probe {i + 1}") for i, p in enumerate(latest.get("probes", []))] + if not names: + names = [f"Probe {i + 1}" for i in range(4)] + return jsonify({"ok": True, "names": names, "source": "fallback"}), 200 + names = settings.get("probeNames", [f"Probe {i + 1}" for i in range(4)]) + return jsonify({"ok": True, "names": names, "source": "cyd"}) + + # PUT — forward to CYD's /api/settings with just the probeNames field. + data = request.get_json() or {} + names = data.get("names", []) + if not isinstance(names, list) or len(names) != 4 or not all(isinstance(n, str) for n in names): + return jsonify({"ok": False, "error": "names must be an array of 4 strings"}), 422 + try: + resp = requests.post( + f"{config.CYD_BASE_URL}/api/settings", + json={"probeNames": names}, + timeout=10, + ) + resp.raise_for_status() + except Exception as e: + app.logger.warning(f"CYD settings update failed: {e}") + return jsonify({"ok": False, "error": f"Unable to update CYD: {e}"}), 502 + + return jsonify({"ok": True, "names": names}) + + @app.route("/api/sessions", methods=["GET", "POST"]) def sessions(): if request.method == "POST": diff --git a/frontend/static/app.js b/frontend/static/app.js index a9dee35..f4afed1 100644 --- a/frontend/static/app.js +++ b/frontend/static/app.js @@ -8,10 +8,13 @@ const PRESETS = [ ]; let activeSession = null; +let viewedSession = null; +let allSessions = []; let latestReading = null; let setpoints = []; let chartInstance = null; let cardsInitialized = false; +let probeNames = [...DEFAULT_PROBE_NAMES]; async function api(path, options = {}) { const resp = await fetch(path, options); @@ -40,7 +43,7 @@ function createProbeCards() { card.innerHTML = `
No past sessions yet.
'; + return; + } + listEl.innerHTML = past.map(s => { + const isViewed = viewedSession && viewedSession.id === s.id; + const started = formatTime(s.started_at); + const ended = formatTime(s.ended_at); + return ` + + `; + }).join(''); + + listEl.querySelectorAll('.session-row').forEach(btn => { + btn.addEventListener('click', () => { + const id = parseInt(btn.dataset.id, 10); + viewedSession = allSessions.find(s => s.id === id) || null; + renderSessionList(); + loadSetpoints(); + refreshChart(); + updateChartHeader(); + }); + }); +} + +function escapeHtml(s) { + return String(s).replace(/[&<>"']/g, c => ({ + '&': '&', '<': '<', '>': '>', '"': '"', "'": ''', + }[c])); +} + +function updateChartHeader() { + const headerEl = document.getElementById('chart-session-label'); + if (!headerEl) return; + if (!viewedSession) { + headerEl.textContent = 'No session'; + return; + } + if (activeSession && viewedSession.id === activeSession.id) { + headerEl.textContent = `Live — ${viewedSession.name}`; + } else { + headerEl.textContent = `Viewing past — ${viewedSession.name}`; + } } async function loadSetpoints() { - if (!activeSession) { + if (!viewedSession) { setpoints = []; + updateChartHeader(); return; } - setpoints = await api(`/api/setpoints/${activeSession.id}`); + setpoints = await api(`/api/setpoints/${viewedSession.id}`); + updateChartHeader(); } async function refreshDashboard() { @@ -221,13 +351,19 @@ async function refreshDashboard() { } async function refreshChart() { - if (!activeSession || !chartInstance) return; + if (!chartInstance) return; + if (!viewedSession) { + chartInstance.data.labels = []; + chartInstance.data.datasets = []; + chartInstance.update(); + return; + } const range = parseInt(document.getElementById('chart-range').value, 10); - const data = await api(`/api/readings/${activeSession.id}?minutes=${range}`); + const data = await api(`/api/readings/${viewedSession.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}`, + label: probeNames[idx] || (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], @@ -271,7 +407,7 @@ 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', { + const result = await api('/api/sessions', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name, notes }), @@ -279,8 +415,11 @@ document.getElementById('new-session-form').addEventListener('submit', async (e) document.getElementById('session-name').value = ''; document.getElementById('session-notes').value = ''; await loadSessions(); + // Switch the chart to the newly created active session. + viewedSession = allSessions.find(s => s.id === result.session_id) || activeSession; await loadSetpoints(); await refreshChart(); + renderSessionList(); }); document.getElementById('end-session-btn').addEventListener('click', async () => { @@ -291,6 +430,7 @@ document.getElementById('end-session-btn').addEventListener('click', async () => body: JSON.stringify({ end: true }), }); await loadSessions(); + // If we were viewing the just-ended session, keep viewing it (now a past session). await loadSetpoints(); await refreshChart(); }); @@ -300,6 +440,7 @@ document.getElementById('chart-range').addEventListener('change', refreshChart); async function init() { initChart(); + await loadProbeNames(); await loadSessions(); await loadSetpoints(); // First call creates the cards; subsequent calls only update values. @@ -307,7 +448,7 @@ async function init() { await refreshChart(); setInterval(refreshDashboard, 5000); - setInterval(() => { if (activeSession) refreshChart(); }, 30000); + setInterval(() => { if (viewedSession) refreshChart(); }, 30000); } init(); diff --git a/frontend/static/style.css b/frontend/static/style.css index a670a52..b441d65 100644 --- a/frontend/static/style.css +++ b/frontend/static/style.css @@ -139,6 +139,25 @@ main { .probe-card .name { font-weight: bold; font-size: 1.1rem; + cursor: text; + padding: 0 0.15rem; + border-radius: 4px; +} + +.probe-card .name:hover { + background: #2a2a2a; +} + +.probe-card .name .rename-input { + font: inherit; + font-weight: bold; + color: var(--text); + background: #2a2a2a; + border: 1px solid var(--accent); + border-radius: 4px; + padding: 0.1rem 0.3rem; + width: 10ch; + max-width: 100%; } .probe-card .target { @@ -212,6 +231,73 @@ main { cursor: pointer; } +.chart-container { + position: relative; + width: 100%; + height: 320px; +} + +.chart-container canvas { + width: 100% !important; + height: 100% !important; + display: block; +} + +.muted { color: var(--muted); } + +#chart-session-label { + font-size: 0.85rem; + font-weight: normal; + margin-left: 0.5rem; +} + +#past-sessions { + display: flex; + flex-direction: column; + gap: 0.4rem; + max-height: 280px; + overflow-y: auto; +} + +#past-sessions p { + margin: 0.5rem 0; +} + +.session-row { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 0.15rem; + background: #2a2a2a; + border: 1px solid #333; + border-left: 4px solid var(--muted); + color: var(--text); + text-align: left; + padding: 0.6rem 0.8rem; + border-radius: 6px; + cursor: pointer; + font: inherit; +} + +.session-row:hover { + border-left-color: var(--accent); + background: #333; +} + +.session-row.viewed { + border-left-color: var(--accent); + background: #2f2520; +} + +.session-row .session-name { + font-weight: 600; +} + +.session-row .session-times { + font-size: 0.8rem; + color: var(--muted); +} + footer { text-align: center; padding: 1rem; diff --git a/frontend/templates/index.html b/frontend/templates/index.html index 003d9c3..3f5cab0 100644 --- a/frontend/templates/index.html +++ b/frontend/templates/index.html @@ -28,7 +28,12 @@ - + + + +Loading…