diff --git a/server/app.py b/server/app.py index 6a23130..489e799 100644 --- a/server/app.py +++ b/server/app.py @@ -788,8 +788,27 @@ def api_device_config_push(): Pi pushes local config changes to server. Accepts a partial update: any of owner_name, car_model, device_name, avatar_path, odometer_baseline. + odometer_baseline is Pi→Server only — the portal cannot edit it. """ body = request.get_json(silent=True) or {} + + # Handle odometer_baseline separately — Pi→Server only. + odometer = body.pop("odometer_baseline", None) + if odometer is not None: + try: + odometer = float(odometer) + if odometer < 0 or odometer > 999999: + return jsonify(error="Invalid device config.", + fields={"odometer_baseline": "Must be between 0 and 999,999 km."}), 400 + except (TypeError, ValueError): + return jsonify(error="Invalid device config.", + fields={"odometer_baseline": "Must be a number."}), 400 + with db.get_cursor(commit=True) as cur: + cur.execute( + "UPDATE devices SET odometer_baseline = %s, config_version = config_version + 1, config_updated_at = now() WHERE id = %s", + (odometer, g.device_id), + ) + success, errors = device_config.update_device_config(g.device_id, body) if not success: return jsonify(error="Invalid device config.", fields=errors), 400 diff --git a/server/modules/device_config.py b/server/modules/device_config.py index 7fd44aa..6a0fe28 100644 --- a/server/modules/device_config.py +++ b/server/modules/device_config.py @@ -42,7 +42,8 @@ def update_device_config(device_id, data): car_model = data.get("car_model", "").strip() if "car_model" in data else None device_name = data.get("device_name", "").strip() if "device_name" in data else None avatar_path = data.get("avatar_path", "").strip() if "avatar_path" in data else None - odometer_baseline = data.get("odometer_baseline") if "odometer_baseline" in data else None + # odometer_baseline is NOT accepted here — it's Pi→Server only, + # handled directly in the /api/device-config/push endpoint. if owner_name is not None and len(owner_name) > 100: errors["owner_name"] = "Owner name must be 1-100 characters." @@ -52,19 +53,12 @@ def update_device_config(device_id, data): errors["device_name"] = "Device name must be 1-100 characters." if avatar_path is not None and len(avatar_path) > 255: errors["avatar_path"] = "Avatar path must be 1-255 characters." - if odometer_baseline is not None: - try: - odometer_baseline = float(odometer_baseline) - if odometer_baseline < 0 or odometer_baseline > 999999: - errors["odometer_baseline"] = "Must be between 0 and 999,999 km." - except (TypeError, ValueError): - errors["odometer_baseline"] = "Must be a number." if errors: return False, errors with db.get_cursor(commit=True) as cur: - changed = owner_name is not None or car_model is not None or device_name is not None or avatar_path is not None or odometer_baseline is not None + changed = owner_name is not None or car_model is not None or device_name is not None or avatar_path is not None if changed: cur.execute( "UPDATE devices SET config_version = config_version + 1, config_updated_at = now() WHERE id = %s", @@ -79,7 +73,5 @@ def update_device_config(device_id, data): cur.execute("UPDATE devices SET device_name = %s WHERE id = %s", (device_name, device_id)) if avatar_path is not None: cur.execute("UPDATE devices SET avatar_path = %s WHERE id = %s", (avatar_path, device_id)) - if odometer_baseline is not None: - cur.execute("UPDATE devices SET odometer_baseline = %s WHERE id = %s", (odometer_baseline, device_id)) return True, {} diff --git a/server/templates/device_settings.html b/server/templates/device_settings.html index 602f030..641d0b4 100644 --- a/server/templates/device_settings.html +++ b/server/templates/device_settings.html @@ -104,8 +104,8 @@
- -
Enter the car's current odometer reading. The app adds trip distances to this baseline. Synced to the Pi on next config sync.
+ +
Synced from the Pi — cannot be edited here. The Pi adds trip distances to this baseline.
diff --git a/static/js/portal-device-settings.js b/static/js/portal-device-settings.js index 164fe2c..57f1e7e 100644 --- a/static/js/portal-device-settings.js +++ b/static/js/portal-device-settings.js @@ -28,9 +28,12 @@ const fields = { car_model: document.getElementById('car_model'), device_name: document.getElementById('device_name'), avatar_path: document.getElementById('avatar_path'), - odometer_baseline: document.getElementById('odometer_baseline'), }; +// Read-only display field — populated from server config, never sent on save. +// Odometer is Pi→Server only: the Pi pushes its baseline, the portal just shows it. +const odometerField = document.getElementById('odometer_baseline'); + let devices = []; let selectedDeviceId = null; let initialState = null; @@ -57,7 +60,6 @@ function currentState() { car_model: fields.car_model.value.trim(), device_name: fields.device_name.value.trim(), avatar_path: fields.avatar_path.value.trim(), - odometer_baseline: Number(fields.odometer_baseline.value) || 0, }; } @@ -117,7 +119,7 @@ function populateDevice(device, config) { fields.car_model.value = config.car_model || ''; fields.device_name.value = config.device_name || ''; fields.avatar_path.value = config.avatar_path || ''; - fields.odometer_baseline.value = (config.odometer_baseline || 0).toFixed(1); + odometerField.value = (config.odometer_baseline || 0).toFixed(1); initialState = currentState(); credentialFields.deviceName.value = device.name; renderDeviceList();