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.
This commit is contained in:
+214
@@ -0,0 +1,214 @@
|
||||
import sqlite3
|
||||
from contextlib import contextmanager
|
||||
from pathlib import Path
|
||||
from datetime import datetime, timezone
|
||||
|
||||
import config
|
||||
|
||||
INIT_SQL = """
|
||||
CREATE TABLE IF NOT EXISTS sessions (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL,
|
||||
started_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
|
||||
ended_at TEXT,
|
||||
notes TEXT
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS readings (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
session_id INTEGER NOT NULL,
|
||||
received_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
|
||||
device_id TEXT,
|
||||
mac TEXT,
|
||||
ble_connected INTEGER,
|
||||
battery INTEGER,
|
||||
unit TEXT,
|
||||
probe1_temp REAL,
|
||||
probe1_connected INTEGER,
|
||||
probe2_temp REAL,
|
||||
probe2_connected INTEGER,
|
||||
probe3_temp REAL,
|
||||
probe3_connected INTEGER,
|
||||
probe4_temp REAL,
|
||||
probe4_connected INTEGER,
|
||||
FOREIGN KEY (session_id) REFERENCES sessions(id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS setpoints (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
session_id INTEGER NOT NULL,
|
||||
probe_number INTEGER NOT NULL,
|
||||
label TEXT,
|
||||
target_temp REAL NOT NULL,
|
||||
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
|
||||
FOREIGN KEY (session_id) REFERENCES sessions(id),
|
||||
UNIQUE(session_id, probe_number)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_readings_session_time
|
||||
ON readings(session_id, received_at);
|
||||
"""
|
||||
|
||||
|
||||
def init_db():
|
||||
Path(config.DATABASE_PATH).parent.mkdir(parents=True, exist_ok=True)
|
||||
with get_connection() as conn:
|
||||
conn.executescript(INIT_SQL)
|
||||
conn.commit()
|
||||
|
||||
|
||||
@contextmanager
|
||||
def get_connection():
|
||||
conn = sqlite3.connect(config.DATABASE_PATH)
|
||||
conn.row_factory = sqlite3.Row
|
||||
try:
|
||||
yield conn
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
def utc_now_iso():
|
||||
return datetime.now(timezone.utc).isoformat()
|
||||
|
||||
|
||||
def create_session(name: str, notes: str = ""):
|
||||
with get_connection() as conn:
|
||||
cursor = conn.execute(
|
||||
"INSERT INTO sessions (name, notes) VALUES (?, ?)",
|
||||
(name, notes),
|
||||
)
|
||||
conn.commit()
|
||||
return cursor.lastrowid
|
||||
|
||||
|
||||
def get_active_session():
|
||||
with get_connection() as conn:
|
||||
row = conn.execute(
|
||||
"SELECT * FROM sessions WHERE ended_at IS NULL ORDER BY started_at DESC LIMIT 1"
|
||||
).fetchone()
|
||||
return dict(row) if row else None
|
||||
|
||||
|
||||
def end_session(session_id: int):
|
||||
with get_connection() as conn:
|
||||
conn.execute(
|
||||
"UPDATE sessions SET ended_at = ? WHERE id = ?",
|
||||
(utc_now_iso(), session_id),
|
||||
)
|
||||
conn.commit()
|
||||
|
||||
|
||||
def update_session_notes(session_id: int, notes: str):
|
||||
with get_connection() as conn:
|
||||
conn.execute(
|
||||
"UPDATE sessions SET notes = ? WHERE id = ?",
|
||||
(notes, session_id),
|
||||
)
|
||||
conn.commit()
|
||||
|
||||
|
||||
def insert_reading(session_id: int, payload: dict):
|
||||
probes = payload.get("probes", [])
|
||||
temps = []
|
||||
connected = []
|
||||
for i in range(4):
|
||||
if i < len(probes) and probes[i].get("connected"):
|
||||
temps.append(probes[i].get("temperature"))
|
||||
connected.append(1)
|
||||
else:
|
||||
temps.append(None)
|
||||
connected.append(0)
|
||||
|
||||
with get_connection() as conn:
|
||||
conn.execute(
|
||||
"""
|
||||
INSERT INTO readings (
|
||||
session_id, received_at, device_id, mac, ble_connected, battery, unit,
|
||||
probe1_temp, probe1_connected,
|
||||
probe2_temp, probe2_connected,
|
||||
probe3_temp, probe3_connected,
|
||||
probe4_temp, probe4_connected
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
(
|
||||
session_id,
|
||||
utc_now_iso(),
|
||||
payload.get("deviceId"),
|
||||
payload.get("mac"),
|
||||
1 if payload.get("connected") else 0,
|
||||
payload.get("battery"),
|
||||
payload.get("unit"),
|
||||
temps[0], connected[0],
|
||||
temps[1], connected[1],
|
||||
temps[2], connected[2],
|
||||
temps[3], connected[3],
|
||||
),
|
||||
)
|
||||
conn.commit()
|
||||
|
||||
|
||||
def get_latest_reading(session_id: int):
|
||||
with get_connection() as conn:
|
||||
row = conn.execute(
|
||||
"SELECT * FROM readings WHERE session_id = ? ORDER BY received_at DESC LIMIT 1",
|
||||
(session_id,),
|
||||
).fetchone()
|
||||
return dict(row) if row else None
|
||||
|
||||
|
||||
def get_readings(session_id: int, minutes: int = 60, probe_number: int = None):
|
||||
with get_connection() as conn:
|
||||
if probe_number is not None:
|
||||
column = f"probe{probe_number}_temp"
|
||||
rows = conn.execute(
|
||||
f"""
|
||||
SELECT received_at, {column} AS temp, probe{probe_number}_connected AS connected
|
||||
FROM readings
|
||||
WHERE session_id = ?
|
||||
AND received_at > datetime('now', '-{minutes} minutes')
|
||||
AND {column} IS NOT NULL
|
||||
ORDER BY received_at ASC
|
||||
""",
|
||||
(session_id,),
|
||||
).fetchall()
|
||||
else:
|
||||
rows = conn.execute(
|
||||
f"""
|
||||
SELECT received_at,
|
||||
probe1_temp, probe1_connected,
|
||||
probe2_temp, probe2_connected,
|
||||
probe3_temp, probe3_connected,
|
||||
probe4_temp, probe4_connected
|
||||
FROM readings
|
||||
WHERE session_id = ?
|
||||
AND received_at > datetime('now', '-{minutes} minutes')
|
||||
ORDER BY received_at ASC
|
||||
""",
|
||||
(session_id,),
|
||||
).fetchall()
|
||||
return [dict(row) for row in rows]
|
||||
|
||||
|
||||
def set_setpoint(session_id: int, probe_number: int, label: str, target_temp: float):
|
||||
with get_connection() as conn:
|
||||
conn.execute(
|
||||
"""
|
||||
INSERT INTO setpoints (session_id, probe_number, label, target_temp)
|
||||
VALUES (?, ?, ?, ?)
|
||||
ON CONFLICT(session_id, probe_number) DO UPDATE SET
|
||||
label = excluded.label,
|
||||
target_temp = excluded.target_temp,
|
||||
created_at = excluded.created_at
|
||||
""",
|
||||
(session_id, probe_number, label, target_temp),
|
||||
)
|
||||
conn.commit()
|
||||
|
||||
|
||||
def get_setpoints(session_id: int):
|
||||
with get_connection() as conn:
|
||||
rows = conn.execute(
|
||||
"SELECT * FROM setpoints WHERE session_id = ? ORDER BY probe_number",
|
||||
(session_id,),
|
||||
).fetchall()
|
||||
return [dict(row) for row in rows]
|
||||
Reference in New Issue
Block a user