Files
ESP32-ThermoPro-Bridge/frontend/app.py
T
Keith Solomon 9e32b1b179 feat: add PWA frontend for CYD ThermoPro dashboard
Add a Flask backend + SQLite + HTML/CSS/JS PWA that:
- Polls the CYD /api/latest endpoint
- Stores readings, sessions, and freeform notes in SQLite
- Displays probe cards with target setpoints and preset options
- Renders temperature history with Chart.js
- Serves a PWA manifest and dark-themed dashboard

Also update cyd-bridge/README.md with verified bringup instructions.
2026-07-20 20:14:48 -05:00

156 lines
4.9 KiB
Python

import json
import threading
import time
from datetime import datetime, timezone
import requests
from flask import Flask, jsonify, request, render_template
from waitress import serve
import config
import db
app = Flask(__name__, static_folder="static", template_folder="templates")
db.init_db()
_poller_thread = None
_stop_polling = threading.Event()
def fetch_cyd():
try:
resp = requests.get(
f"{config.CYD_BASE_URL}/api/latest",
timeout=10,
)
resp.raise_for_status()
return resp.json()
except Exception as e:
app.logger.warning(f"CYD fetch failed: {e}")
return None
def poller_loop():
while not _stop_polling.is_set():
session = db.get_active_session()
if session:
payload = fetch_cyd()
if payload:
db.insert_reading(session["id"], payload)
time.sleep(config.POLL_INTERVAL)
def start_poller():
global _poller_thread
if _poller_thread is None or not _poller_thread.is_alive():
_stop_polling.clear()
_poller_thread = threading.Thread(target=poller_loop, daemon=True)
_poller_thread.start()
@app.route("/")
def index():
return render_template("index.html")
@app.route("/api/status")
def api_status():
"""Backend status plus current CYD reading."""
session = db.get_active_session()
latest = db.get_latest_reading(session["id"]) if session else None
latest_proxy = fetch_cyd()
return jsonify({
"ok": True,
"cyd_base_url": config.CYD_BASE_URL,
"poll_interval": config.POLL_INTERVAL,
"active_session": session,
"latest_reading": latest_proxy or latest,
})
@app.route("/api/cyd/latest")
def api_cyd_latest():
"""Proxy to the CYD's own /api/latest."""
payload = fetch_cyd()
if payload is None:
return jsonify({"ok": False, "error": "Unable to reach CYD"}), 502
return jsonify(payload)
@app.route("/api/sessions", methods=["GET", "POST"])
def sessions():
if request.method == "POST":
data = request.get_json() or {}
name = data.get("name", f"Cook {datetime.now(timezone.utc).isoformat()[:19]}")
notes = data.get("notes", "")
# End any currently active session first.
active = db.get_active_session()
if active:
db.end_session(active["id"])
session_id = db.create_session(name, notes)
return jsonify({"ok": True, "session_id": session_id}), 201
with db.get_connection() as conn:
rows = conn.execute(
"SELECT * FROM sessions ORDER BY started_at DESC"
).fetchall()
return jsonify([dict(row) for row in rows])
@app.route("/api/sessions/<int:session_id>", methods=["GET", "PUT", "DELETE"])
def session_detail(session_id):
if request.method == "GET":
with db.get_connection() as conn:
row = conn.execute(
"SELECT * FROM sessions WHERE id = ?", (session_id,)
).fetchone()
if not row:
return jsonify({"ok": False, "error": "Session not found"}), 404
return jsonify(dict(row))
if request.method == "PUT":
data = request.get_json() or {}
if "notes" in data:
db.update_session_notes(session_id, data["notes"])
if data.get("end"):
db.end_session(session_id)
return jsonify({"ok": True})
if request.method == "DELETE":
with db.get_connection() as conn:
conn.execute("DELETE FROM readings WHERE session_id = ?", (session_id,))
conn.execute("DELETE FROM setpoints WHERE session_id = ?", (session_id,))
conn.execute("DELETE FROM sessions WHERE id = ?", (session_id,))
conn.commit()
return jsonify({"ok": True})
@app.route("/api/readings/<int:session_id>")
def readings(session_id):
minutes = request.args.get("minutes", config.DEFAULT_CHART_MINUTES, type=int)
probe = request.args.get("probe", type=int)
data = db.get_readings(session_id, minutes=minutes, probe_number=probe)
return jsonify(data)
@app.route("/api/setpoints/<int:session_id>", methods=["GET", "POST"])
def setpoints(session_id):
if request.method == "POST":
data = request.get_json() or {}
probe_number = data.get("probe_number")
label = data.get("label", "")
target_temp = data.get("target_temp")
if probe_number is None or target_temp is None:
return jsonify({"ok": False, "error": "probe_number and target_temp required"}), 422
db.set_setpoint(session_id, probe_number, label, float(target_temp))
return jsonify({"ok": True})
return jsonify(db.get_setpoints(session_id))
if __name__ == "__main__":
start_poller()
print(f"Starting server on http://0.0.0.0:5000")
print(f"Polling CYD at {config.CYD_BASE_URL} every {config.POLL_INTERVAL}s")
serve(app, host="0.0.0.0", port=5000)