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.
156 lines
4.9 KiB
Python
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)
|