NitinBot001 commited on
Commit
a67c4e5
Β·
verified Β·
1 Parent(s): 7dc098a

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +127 -320
app.py CHANGED
@@ -1,24 +1,3 @@
1
- """
2
- Flask + SQLite Telemetry Server v2.0
3
- ESP32 (INA219 + Potentiometer) β†’ Flask API β†’ SQLite DB β†’ Dashboard
4
-
5
- Install: pip install flask flask-cors
6
- Run: python server.py
7
- Dashboard: http://localhost:8000
8
-
9
- DB file: telemetry.db (auto-created on first run)
10
-
11
- POST /api/telemetry ← ESP32 ΰ€―ΰ€Ήΰ€Ύΰ€ data ΰ€­ΰ₯‡ΰ€œΰ€€ΰ€Ύ ΰ€Ήΰ₯ˆ
12
- GET /api/latest ← latest 1 record
13
- GET /api/history ← last N records + stats
14
- GET /api/track ← position + speed only (lightweight)
15
- GET /api/stats ← session summary + energy
16
- GET /api/devices ← all registered devices
17
- GET /api/export ← CSV download
18
- DELETE /api/clear ← data clear
19
- GET /health ← server health check
20
- """
21
-
22
  from flask import Flask, request, jsonify, send_file, g, Response
23
  from flask_cors import CORS
24
  from datetime import datetime
@@ -28,22 +7,22 @@ import os
28
  import csv
29
  import io
30
 
31
- # ─── CONFIG ──────────────────────────────────────────────────
32
- HOST = "0.0.0.0"
33
- PORT = 7860
34
- DB_PATH = os.path.join(os.path.dirname(__file__), "telemetry.db")
 
 
35
 
36
  app = Flask(__name__)
37
  CORS(app)
38
 
39
- # ─── DATABASE ─────────────────────────────────────────────────
40
 
41
  def get_db():
42
  if "db" not in g:
43
- g.db = sqlite3.connect(DB_PATH, detect_types=sqlite3.PARSE_DECLTYPES)
44
  g.db.row_factory = sqlite3.Row
45
- g.db.execute("PRAGMA journal_mode=WAL")
46
- g.db.execute("PRAGMA synchronous=NORMAL")
47
  return g.db
48
 
49
 
@@ -56,336 +35,171 @@ def close_db(exc=None):
56
 
57
  def init_db():
58
  with sqlite3.connect(DB_PATH) as db:
59
- db.execute("PRAGMA journal_mode=WAL")
60
- db.execute("""
61
- CREATE TABLE IF NOT EXISTS telemetry (
62
- id INTEGER PRIMARY KEY AUTOINCREMENT,
63
- device_id TEXT NOT NULL,
64
- server_time TEXT NOT NULL,
65
- server_unix REAL NOT NULL,
66
- uptime_sec INTEGER DEFAULT 0,
67
-
68
- -- Motion
69
- speed_pct REAL NOT NULL,
70
- speed_ups REAL NOT NULL,
71
- pos_x REAL NOT NULL,
72
- pos_y REAL NOT NULL DEFAULT 0,
73
- pos_z REAL NOT NULL DEFAULT 0,
74
- total_distance REAL NOT NULL,
75
-
76
- -- Power (from INA219)
77
- voltage_V REAL NOT NULL,
78
- current_mA REAL NOT NULL,
79
- current_A REAL NOT NULL,
80
- power_mW REAL NOT NULL,
81
- power_W REAL NOT NULL,
82
- drawn_mW REAL NOT NULL DEFAULT 0,
83
- generated_mW REAL NOT NULL DEFAULT 0,
84
- samples INTEGER NOT NULL DEFAULT 25,
85
- interval_sec INTEGER NOT NULL DEFAULT 5
86
- )
87
- """)
88
  db.execute("""
89
- CREATE INDEX IF NOT EXISTS idx_device_time
90
- ON telemetry (device_id, server_unix DESC)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
91
  """)
92
  db.commit()
93
- print(f"[DB] SQLite ready β†’ {DB_PATH}")
 
94
 
95
 
96
- def row_to_dict(row):
97
- return dict(row)
 
98
 
99
 
100
- # ─── HELPERS ──────────────────────────────────────────────────
101
 
102
  def query(sql, params=(), one=False):
103
  cur = get_db().execute(sql, params)
104
  rows = cur.fetchall()
105
- result = [row_to_dict(r) for r in rows]
106
- return result[0] if (one and result) else (None if one else result)
107
 
108
 
109
- def compute_stats(rows):
110
- if not rows:
111
- return {}
112
- n = len(rows)
113
- return {
114
- "speed_avg_pct": round(sum(r["speed_pct"] for r in rows) / n, 2),
115
- "speed_max_pct": round(max(r["speed_pct"] for r in rows), 2),
116
- "power_avg_mW": round(sum(r["power_mW"] for r in rows) / n, 2),
117
- "power_max_mW": round(max(r["power_mW"] for r in rows), 2),
118
- "voltage_avg_V": round(sum(r["voltage_V"] for r in rows) / n, 3),
119
- "pos_x_latest": round(rows[-1]["pos_x"], 3),
120
- "pos_x_range": round(
121
- max(r["pos_x"] for r in rows) - min(r["pos_x"] for r in rows), 3
122
- ),
123
- }
124
-
125
-
126
- def safe_float(data, key, default=0.0):
127
- """Safely extract float from dict, return default if missing/invalid."""
128
  try:
129
  return float(data.get(key, default))
130
- except (TypeError, ValueError):
131
  return default
132
 
133
 
134
- # ─── ROUTES ───────────────────────────────────────────────────
135
 
136
- @app.post("/api/telemetry")
137
- def receive_telemetry():
138
- """
139
- ESP32 POSTs JSON here every interval.
140
-
141
- Required body:
142
- {
143
- "device_id" : "esp32-tracker-01",
144
- "uptime_sec" : 120, ← optional (default 0)
145
- "speed_pct" : 73.4, ← 0–100
146
- "speed_ups" : 7.34, ← speed_pct / 10
147
- "position" : { "x": 182.5, "y": 0, "z": 0 },
148
- "total_distance": 182.5,
149
- "power": {
150
- "voltage_V" : 3.712,
151
- "current_mA" : 245.6,
152
- "current_A" : 0.2456,
153
- "power_mW" : 912.5,
154
- "power_W" : 0.9125,
155
- "drawn_mW" : 912.5,
156
- "generated_mW" : 0.0,
157
- "samples" : 25,
158
- "interval_sec" : 5
159
- }
160
- }
161
- """
162
- data = request.get_json(force=True, silent=True)
163
- if not data:
164
- return jsonify({"error": "Invalid JSON"}), 400
165
 
166
- # Required fields validation
167
- required = ["device_id", "speed_pct", "speed_ups", "position", "total_distance", "power"]
168
- missing = [k for k in required if k not in data]
169
- if missing:
170
- return jsonify({"error": f"Missing fields: {missing}"}), 400
171
 
172
- pos = data["position"]
173
- pwr = data["power"]
174
 
175
- # Required power fields
176
- required_pwr = ["voltage_V", "current_mA", "current_A", "power_mW", "power_W"]
177
- missing_pwr = [k for k in required_pwr if k not in pwr]
178
- if missing_pwr:
179
- return jsonify({"error": f"Missing power fields: {missing_pwr}"}), 400
180
 
181
- now = datetime.now()
182
- server_time = now.isoformat(timespec="milliseconds")
183
  server_unix = time.time()
184
 
185
  db = get_db()
 
186
  db.execute("""
187
- INSERT INTO telemetry (
188
- device_id, server_time, server_unix, uptime_sec,
189
- speed_pct, speed_ups,
190
- pos_x, pos_y, pos_z, total_distance,
191
- voltage_V, current_mA, current_A,
192
- power_mW, power_W, drawn_mW, generated_mW,
193
- samples, interval_sec
194
- ) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
 
195
  """, (
196
- data["device_id"],
197
  server_time,
198
  server_unix,
199
  data.get("uptime_sec", 0),
200
- float(data["speed_pct"]),
201
- float(data["speed_ups"]),
 
 
202
  safe_float(pos, "x"),
203
  safe_float(pos, "y"),
204
  safe_float(pos, "z"),
205
- float(data["total_distance"]),
206
- float(pwr["voltage_V"]),
207
- float(pwr["current_mA"]),
208
- float(pwr["current_A"]),
209
- float(pwr["power_mW"]),
210
- float(pwr["power_W"]),
211
- safe_float(pwr, "drawn_mW"),
212
- safe_float(pwr, "generated_mW"),
213
- int(pwr.get("samples", 25)),
214
- int(pwr.get("interval_sec", 5)),
 
 
215
  ))
216
- db.commit()
217
 
218
- print(
219
- f"[{server_time[:19]}] {data['device_id']} | "
220
- f"Spd={float(data['speed_pct']):.1f}% | "
221
- f"X={safe_float(pos,'x'):.3f} | "
222
- f"V={float(pwr['voltage_V']):.3f}V | "
223
- f"P={float(pwr['power_mW']):.2f}mW"
224
- )
225
 
226
- return jsonify({"status": "ok", "server_time": server_time}), 201
227
 
228
 
229
  @app.get("/api/latest")
230
- def get_latest():
231
- device_id = request.args.get("device_id")
232
- if device_id:
233
- row = query(
234
- "SELECT * FROM telemetry WHERE device_id=? ORDER BY server_unix DESC LIMIT 1",
235
- (device_id,), one=True
236
- )
237
- else:
238
- row = query(
239
- "SELECT * FROM telemetry ORDER BY server_unix DESC LIMIT 1",
240
- one=True
241
- )
242
 
243
  if not row:
244
- return jsonify({"error": "No data yet"}), 404
245
 
246
- total = query("SELECT COUNT(*) AS n FROM telemetry", one=True)["n"]
247
- return jsonify({"latest": row, "total_stored": total})
248
 
249
 
250
  @app.get("/api/history")
251
- def get_history():
252
- limit = min(int(request.args.get("limit", 200)), 1000) # max 1000
253
- device_id = request.args.get("device_id")
254
-
255
- if device_id:
256
- rows = query(
257
- "SELECT * FROM telemetry WHERE device_id=? ORDER BY server_unix DESC LIMIT ?",
258
- (device_id, limit)
259
- )
260
- else:
261
- rows = query(
262
- "SELECT * FROM telemetry ORDER BY server_unix DESC LIMIT ?",
263
- (limit,)
264
- )
265
-
266
- rows = list(reversed(rows))
267
- return jsonify({
268
- "readings": rows,
269
- "count": len(rows),
270
- "stats": compute_stats(rows),
271
- })
272
-
273
-
274
- @app.get("/api/track")
275
- def get_track():
276
- """Position + speed only β€” lightweight for real-time visualization."""
277
- limit = min(int(request.args.get("limit", 500)), 2000)
278
- device_id = request.args.get("device_id")
279
 
280
- if device_id:
281
- rows = query(
282
- "SELECT server_unix, pos_x, pos_y, speed_pct FROM telemetry "
283
- "WHERE device_id=? ORDER BY server_unix DESC LIMIT ?",
284
- (device_id, limit)
285
- )
286
- else:
287
- rows = query(
288
- "SELECT server_unix, pos_x, pos_y, speed_pct FROM telemetry "
289
- "ORDER BY server_unix DESC LIMIT ?",
290
- (limit,)
291
- )
292
-
293
- rows = list(reversed(rows))
294
- track = [{
295
- "t": r["server_unix"],
296
- "x": r["pos_x"],
297
- "y": r["pos_y"],
298
- "spd": r["speed_pct"]
299
- } for r in rows]
300
- return jsonify({"track": track, "points": len(track)})
301
-
302
-
303
- @app.get("/api/stats")
304
- def get_stats():
305
- """Full session summary with energy calculation."""
306
- device_id = request.args.get("device_id")
307
-
308
- if device_id:
309
- rows = query(
310
- "SELECT * FROM telemetry WHERE device_id=? ORDER BY server_unix ASC",
311
- (device_id,)
312
- )
313
- else:
314
- rows = query("SELECT * FROM telemetry ORDER BY server_unix ASC")
315
-
316
- if not rows:
317
- return jsonify({"error": "No data"}), 404
318
-
319
- n = len(rows)
320
- interval = rows[-1]["interval_sec"] or 5
321
- total_h = (n * interval) / 3600.0
322
- energy_mWh = sum(r["power_mW"] for r in rows) * (interval / 3600.0)
323
 
324
  return jsonify({
325
- "total_packets": n,
326
- "total_time_minutes": round(total_h * 60, 2),
327
- "device_id": rows[-1]["device_id"],
328
- "speed": {
329
- "avg_pct": round(sum(r["speed_pct"] for r in rows) / n, 2),
330
- "max_pct": round(max(r["speed_pct"] for r in rows), 2),
331
- "avg_ups": round(sum(r["speed_ups"] for r in rows) / n, 3),
332
- },
333
- "position": {
334
- "current_x": round(rows[-1]["pos_x"], 3),
335
- "total_distance": round(rows[-1]["total_distance"], 3),
336
- },
337
- "power": {
338
- "voltage_avg_V": round(sum(r["voltage_V"] for r in rows) / n, 3),
339
- "current_avg_mA": round(sum(r["current_mA"] for r in rows) / n, 2),
340
- "power_avg_mW": round(sum(r["power_mW"] for r in rows) / n, 2),
341
- "power_max_mW": round(max(r["power_mW"] for r in rows), 2),
342
- "total_energy_mWh": round(energy_mWh, 4),
343
- "drawn_total_mWh": round(
344
- sum(r["drawn_mW"] for r in rows) * (interval / 3600.0), 4
345
- ),
346
- },
347
- "first_packet": rows[0]["server_time"],
348
- "last_packet": rows[-1]["server_time"],
349
  })
350
 
351
 
352
  @app.get("/api/devices")
353
- def get_devices():
354
- """List all devices with last-seen info and packet count."""
355
  rows = query("""
356
- SELECT device_id,
357
- COUNT(*) AS total_packets,
358
- MAX(server_time) AS last_seen,
359
- MAX(pos_x) AS max_x,
360
- MAX(speed_pct) AS max_speed,
361
- AVG(voltage_V) AS avg_voltage,
362
- AVG(power_mW) AS avg_power
363
- FROM telemetry
364
- GROUP BY device_id
365
  """)
366
- devices = {r["device_id"]: r for r in rows}
367
- return jsonify({"devices": devices, "count": len(devices)})
368
 
369
 
370
  @app.get("/api/export")
371
  def export_csv():
372
- """Download all data as CSV."""
373
- device_id = request.args.get("device_id")
374
 
375
- if device_id:
376
- rows = query(
377
- "SELECT * FROM telemetry WHERE device_id=? ORDER BY server_unix ASC",
378
- (device_id,)
379
- )
380
- filename = f"telemetry_{device_id}.csv"
381
- else:
382
- rows = query("SELECT * FROM telemetry ORDER BY server_unix ASC")
383
- filename = "telemetry_all.csv"
384
 
385
  if not rows:
386
- return jsonify({"error": "No data"}), 404
387
 
388
  out = io.StringIO()
 
389
  writer = csv.DictWriter(out, fieldnames=rows[0].keys())
390
  writer.writeheader()
391
  writer.writerows(rows)
@@ -393,47 +207,40 @@ def export_csv():
393
  return Response(
394
  out.getvalue(),
395
  mimetype="text/csv",
396
- headers={"Content-Disposition": f"attachment; filename={filename}"}
397
  )
398
 
399
 
400
  @app.delete("/api/clear")
401
- def clear_data():
402
- """Delete all records (or by device_id)."""
403
- device_id = request.args.get("device_id")
404
  db = get_db()
405
- if device_id:
406
- db.execute("DELETE FROM telemetry WHERE device_id=?", (device_id,))
407
- msg = f"Cleared data for '{device_id}'"
408
- else:
409
- db.execute("DELETE FROM telemetry")
410
- msg = "All data cleared"
411
  db.commit()
412
- return jsonify({"status": "ok", "message": msg})
 
413
 
414
 
415
  @app.get("/health")
416
  def health():
417
- row = query("SELECT COUNT(*) AS n FROM telemetry", one=True)
 
 
418
  return jsonify({
419
- "status": "ok",
420
- "db": DB_PATH,
421
- "total_records": row["n"] if row else 0,
422
- "server_time": datetime.now().isoformat(timespec="milliseconds"),
423
  })
424
 
425
 
426
  @app.get("/")
427
  def dashboard():
428
- html_path = os.path.join(os.path.dirname(__file__), "dashboard.html")
429
- return send_file(html_path)
430
 
431
 
432
- # ─── MAIN ─────────────────────────────────────────────────────
 
433
  if __name__ == "__main__":
434
- init_db()
435
- print(f"\n[SERVER] Flask running β†’ http://{HOST}:{PORT}")
436
- print(f"[SERVER] Dashboard β†’ http://localhost:{PORT}")
437
- print(f"[SERVER] API telemetry β†’ POST http://localhost:{PORT}/api/telemetry")
438
- print(f"[SERVER] DB path β†’ {DB_PATH}\n")
439
- app.run(host=HOST, port=PORT, debug=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  from flask import Flask, request, jsonify, send_file, g, Response
2
  from flask_cors import CORS
3
  from datetime import datetime
 
7
  import csv
8
  import io
9
 
10
+ # ─── CONFIG ───────────────────────────────────────────
11
+ HOST = "0.0.0.0"
12
+ PORT = 7860
13
+
14
+ BASE_DIR = os.path.dirname(__file__)
15
+ DB_PATH = os.path.join(BASE_DIR, "telemetry.db")
16
 
17
  app = Flask(__name__)
18
  CORS(app)
19
 
20
+ # ─── DATABASE ─────────────────────────────────────────
21
 
22
  def get_db():
23
  if "db" not in g:
24
+ g.db = sqlite3.connect(DB_PATH)
25
  g.db.row_factory = sqlite3.Row
 
 
26
  return g.db
27
 
28
 
 
35
 
36
  def init_db():
37
  with sqlite3.connect(DB_PATH) as db:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
38
  db.execute("""
39
+ CREATE TABLE IF NOT EXISTS telemetry (
40
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
41
+ device_id TEXT,
42
+ server_time TEXT,
43
+ server_unix REAL,
44
+ uptime_sec INTEGER,
45
+
46
+ speed_pct REAL,
47
+ speed_ups REAL,
48
+
49
+ pos_x REAL,
50
+ pos_y REAL,
51
+ pos_z REAL,
52
+ total_distance REAL,
53
+
54
+ voltage_V REAL,
55
+ current_mA REAL,
56
+ current_A REAL,
57
+ power_mW REAL,
58
+ power_W REAL,
59
+ drawn_mW REAL,
60
+ generated_mW REAL,
61
+
62
+ samples INTEGER,
63
+ interval_sec INTEGER
64
+ )
65
  """)
66
  db.commit()
67
+
68
+ print("Database initialized")
69
 
70
 
71
+ # πŸ”΄ IMPORTANT FIX (Gunicorn compatible)
72
+ with app.app_context():
73
+ init_db()
74
 
75
 
76
+ # ─── HELPERS ─────────────────────────────────────────
77
 
78
  def query(sql, params=(), one=False):
79
  cur = get_db().execute(sql, params)
80
  rows = cur.fetchall()
81
+ data = [dict(r) for r in rows]
82
+ return (data[0] if data else None) if one else data
83
 
84
 
85
+ def safe_float(data, key, default=0):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
86
  try:
87
  return float(data.get(key, default))
88
+ except:
89
  return default
90
 
91
 
92
+ # ─── ROUTES ─────────────────────────────────────────
93
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
94
 
95
+ @app.post("/api/telemetry")
96
+ def receive():
97
+ data = request.get_json()
 
 
98
 
99
+ if not data:
100
+ return jsonify({"error": "invalid json"}), 400
101
 
102
+ pos = data.get("position", {})
103
+ pwr = data.get("power", {})
 
 
 
104
 
105
+ now = datetime.now()
106
+ server_time = now.isoformat()
107
  server_unix = time.time()
108
 
109
  db = get_db()
110
+
111
  db.execute("""
112
+ INSERT INTO telemetry (
113
+ device_id, server_time, server_unix, uptime_sec,
114
+ speed_pct, speed_ups,
115
+ pos_x, pos_y, pos_z, total_distance,
116
+ voltage_V, current_mA, current_A,
117
+ power_mW, power_W, drawn_mW, generated_mW,
118
+ samples, interval_sec
119
+ )
120
+ VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
121
  """, (
122
+ data.get("device_id"),
123
  server_time,
124
  server_unix,
125
  data.get("uptime_sec", 0),
126
+
127
+ data.get("speed_pct", 0),
128
+ data.get("speed_ups", 0),
129
+
130
  safe_float(pos, "x"),
131
  safe_float(pos, "y"),
132
  safe_float(pos, "z"),
133
+ data.get("total_distance", 0),
134
+
135
+ pwr.get("voltage_V", 0),
136
+ pwr.get("current_mA", 0),
137
+ pwr.get("current_A", 0),
138
+ pwr.get("power_mW", 0),
139
+ pwr.get("power_W", 0),
140
+ pwr.get("drawn_mW", 0),
141
+ pwr.get("generated_mW", 0),
142
+
143
+ pwr.get("samples", 25),
144
+ pwr.get("interval_sec", 5),
145
  ))
 
146
 
147
+ db.commit()
 
 
 
 
 
 
148
 
149
+ return jsonify({"status": "ok"})
150
 
151
 
152
  @app.get("/api/latest")
153
+ def latest():
154
+ row = query(
155
+ "SELECT * FROM telemetry ORDER BY server_unix DESC LIMIT 1",
156
+ one=True
157
+ )
 
 
 
 
 
 
 
158
 
159
  if not row:
160
+ return jsonify({"error": "no data"}), 404
161
 
162
+ return jsonify(row)
 
163
 
164
 
165
  @app.get("/api/history")
166
+ def history():
167
+ limit = int(request.args.get("limit", 100))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
168
 
169
+ rows = query(
170
+ "SELECT * FROM telemetry ORDER BY server_unix DESC LIMIT ?",
171
+ (limit,)
172
+ )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
173
 
174
  return jsonify({
175
+ "count": len(rows),
176
+ "data": list(reversed(rows))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
177
  })
178
 
179
 
180
  @app.get("/api/devices")
181
+ def devices():
 
182
  rows = query("""
183
+ SELECT device_id,
184
+ COUNT(*) as packets,
185
+ MAX(server_time) as last_seen
186
+ FROM telemetry
187
+ GROUP BY device_id
 
 
 
 
188
  """)
189
+
190
+ return jsonify(rows)
191
 
192
 
193
  @app.get("/api/export")
194
  def export_csv():
 
 
195
 
196
+ rows = query("SELECT * FROM telemetry")
 
 
 
 
 
 
 
 
197
 
198
  if not rows:
199
+ return jsonify({"error": "no data"}), 404
200
 
201
  out = io.StringIO()
202
+
203
  writer = csv.DictWriter(out, fieldnames=rows[0].keys())
204
  writer.writeheader()
205
  writer.writerows(rows)
 
207
  return Response(
208
  out.getvalue(),
209
  mimetype="text/csv",
210
+ headers={"Content-Disposition": "attachment; filename=telemetry.csv"}
211
  )
212
 
213
 
214
  @app.delete("/api/clear")
215
+ def clear():
216
+
 
217
  db = get_db()
218
+ db.execute("DELETE FROM telemetry")
 
 
 
 
 
219
  db.commit()
220
+
221
+ return jsonify({"status": "cleared"})
222
 
223
 
224
  @app.get("/health")
225
  def health():
226
+
227
+ row = query("SELECT COUNT(*) as n FROM telemetry", one=True)
228
+
229
  return jsonify({
230
+ "status": "ok",
231
+ "records": row["n"] if row else 0,
232
+ "db": DB_PATH,
233
+ "time": datetime.now().isoformat()
234
  })
235
 
236
 
237
  @app.get("/")
238
  def dashboard():
239
+ return send_file(os.path.join(BASE_DIR, "dashboard.html"))
 
240
 
241
 
242
+ # ─── LOCAL RUN ───────────────────────────────────────
243
+
244
  if __name__ == "__main__":
245
+ print("Server running at http://localhost:7860")
246
+ app.run(host=HOST, port=PORT)