"""Primary LoraTester server: REST API + web UI.""" import time from pathlib import Path from flask import Flask, jsonify, request, send_from_directory from flask_cors import CORS from core.auth import ANDROID_CLIENT_HEADER, is_android_client from core.config import HOST, PORT from core.models import ChatIn, TelemetryIn from core import storage from core.telemetry_body import telemetry_from_body STATIC_DIR = Path(__file__).resolve().parent / "static" app = Flask(__name__, static_folder=str(STATIC_DIR), static_url_path="/static") CORS(app) storage.init_db() @app.route("/") def index(): resp = send_from_directory(STATIC_DIR, "index.html") resp.headers["Cache-Control"] = "no-store" return resp @app.post("/api/telemetry") def post_telemetry(): if not is_android_client(request.headers): return jsonify({ "error": "telemetry only from Android app", "hint": f"send header {ANDROID_CLIENT_HEADER}: android", }), 403 body = request.get_json(force=True, silent=True) or {} device_id = body.get("device_id") if not device_id: return jsonify({"error": "device_id required"}), 400 try: data = telemetry_from_body({**body, "device_id": device_id}) return jsonify(storage.record_telemetry(data)) except ValueError as e: return jsonify({"error": str(e)}), 400 @app.get("/api/devices") def get_devices(): return jsonify(storage.list_devices()) @app.get("/api/telemetry") def get_telemetry_history(): device_id = request.args.get("device_id") limit = int(request.args.get("limit", 100)) since = _float_or_none(request.args.get("since")) until = _float_or_none(request.args.get("until")) role = request.args.get("role") return jsonify(storage.get_telemetry(device_id, limit, since, until, role)) @app.get("/api/stats/history") def get_stats_history(): device_id = request.args.get("device_id") if not device_id: return jsonify({"error": "device_id required"}), 400 limit = int(request.args.get("limit", 50)) since = _float_or_none(request.args.get("since")) until = _float_or_none(request.args.get("until")) role = request.args.get("role") return jsonify(storage.get_telemetry(device_id, limit, since, until, role)) @app.post("/api/tracks/start") def tracks_start(): if not is_android_client(request.headers): return jsonify({"error": "Android only"}), 403 body = request.get_json(force=True, silent=True) or {} device_id = body.get("device_id") if not device_id: return jsonify({"error": "device_id required"}), 400 try: return jsonify(storage.start_track(str(device_id), body.get("label"))) except ValueError as e: return jsonify({"error": str(e)}), 400 @app.post("/api/tracks//points") def tracks_points(track_id: int): if not is_android_client(request.headers): return jsonify({"error": "Android only"}), 403 body = request.get_json(force=True, silent=True) or {} points = body.get("points") or [] try: return jsonify(storage.add_track_points(track_id, points)) except ValueError as e: return jsonify({"error": str(e)}), 400 @app.post("/api/tracks//finish") def tracks_finish(track_id: int): if not is_android_client(request.headers): return jsonify({"error": "Android only"}), 403 try: return jsonify(storage.finish_track(track_id)) except ValueError as e: return jsonify({"error": str(e)}), 400 @app.get("/api/tracks") def tracks_list(): device_id = request.args.get("device_id") limit = int(request.args.get("limit", 50)) return jsonify(storage.list_tracks(device_id, limit)) @app.get("/api/tracks/") def tracks_get(track_id: int): try: return jsonify(storage.get_track(track_id)) except ValueError as e: return jsonify({"error": str(e)}), 404 @app.post("/api/chat") def post_chat(): body = request.get_json(force=True, silent=True) or {} device_id = body.get("device_id") text = (body.get("text") or "").strip() if not device_id or not text: return jsonify({"error": "device_id and text required"}), 400 data = ChatIn( device_id=str(device_id), text=text, ts=_float_or_none(body.get("ts")), ) return jsonify(storage.add_chat(data)) @app.get("/api/chat") def get_chat(): since = float(request.args.get("since", 0)) limit = int(request.args.get("limit", 200)) return jsonify(storage.get_chat(since, limit)) @app.get("/api/health") def health(): status = storage.db_status() return jsonify({"ok": status["db_ok"], "ts": time.time(), **status}) def _float_or_none(value): if value is None or value == "": return None try: return float(value) except (TypeError, ValueError): return None if __name__ == "__main__": app.run(host=HOST, port=PORT, debug=True)