fix: odometer sync is Pi→Server only — portal displays read-only value from DB
This commit is contained in:
@@ -788,8 +788,27 @@ def api_device_config_push():
|
|||||||
|
|
||||||
Pi pushes local config changes to server. Accepts a partial update:
|
Pi pushes local config changes to server. Accepts a partial update:
|
||||||
any of owner_name, car_model, device_name, avatar_path, odometer_baseline.
|
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 {}
|
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)
|
success, errors = device_config.update_device_config(g.device_id, body)
|
||||||
if not success:
|
if not success:
|
||||||
return jsonify(error="Invalid device config.", fields=errors), 400
|
return jsonify(error="Invalid device config.", fields=errors), 400
|
||||||
|
|||||||
@@ -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
|
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
|
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
|
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:
|
if owner_name is not None and len(owner_name) > 100:
|
||||||
errors["owner_name"] = "Owner name must be 1-100 characters."
|
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."
|
errors["device_name"] = "Device name must be 1-100 characters."
|
||||||
if avatar_path is not None and len(avatar_path) > 255:
|
if avatar_path is not None and len(avatar_path) > 255:
|
||||||
errors["avatar_path"] = "Avatar path must be 1-255 characters."
|
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:
|
if errors:
|
||||||
return False, errors
|
return False, errors
|
||||||
|
|
||||||
with db.get_cursor(commit=True) as cur:
|
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:
|
if changed:
|
||||||
cur.execute(
|
cur.execute(
|
||||||
"UPDATE devices SET config_version = config_version + 1, config_updated_at = now() WHERE id = %s",
|
"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))
|
cur.execute("UPDATE devices SET device_name = %s WHERE id = %s", (device_name, device_id))
|
||||||
if avatar_path is not None:
|
if avatar_path is not None:
|
||||||
cur.execute("UPDATE devices SET avatar_path = %s WHERE id = %s", (avatar_path, device_id))
|
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, {}
|
return True, {}
|
||||||
|
|||||||
@@ -104,8 +104,8 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="col-12 col-md-6">
|
<div class="col-12 col-md-6">
|
||||||
<label class="form-label" for="odometer_baseline">Odometer baseline (km)</label>
|
<label class="form-label" for="odometer_baseline">Odometer baseline (km)</label>
|
||||||
<input class="form-control" id="odometer_baseline" name="odometer_baseline" type="number" inputmode="decimal" min="0" max="999999" step="0.1" disabled>
|
<input class="form-control text-muted" id="odometer_baseline" name="odometer_baseline" type="number" inputmode="decimal" min="0" max="999999" step="0.1" readonly>
|
||||||
<div class="form-text">Enter the car's current odometer reading. The app adds trip distances to this baseline. Synced to the Pi on next config sync.</div>
|
<div class="form-text">Synced from the Pi — cannot be edited here. The Pi adds trip distances to this baseline.</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -28,9 +28,12 @@ const fields = {
|
|||||||
car_model: document.getElementById('car_model'),
|
car_model: document.getElementById('car_model'),
|
||||||
device_name: document.getElementById('device_name'),
|
device_name: document.getElementById('device_name'),
|
||||||
avatar_path: document.getElementById('avatar_path'),
|
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 devices = [];
|
||||||
let selectedDeviceId = null;
|
let selectedDeviceId = null;
|
||||||
let initialState = null;
|
let initialState = null;
|
||||||
@@ -57,7 +60,6 @@ function currentState() {
|
|||||||
car_model: fields.car_model.value.trim(),
|
car_model: fields.car_model.value.trim(),
|
||||||
device_name: fields.device_name.value.trim(),
|
device_name: fields.device_name.value.trim(),
|
||||||
avatar_path: fields.avatar_path.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.car_model.value = config.car_model || '';
|
||||||
fields.device_name.value = config.device_name || '';
|
fields.device_name.value = config.device_name || '';
|
||||||
fields.avatar_path.value = config.avatar_path || '';
|
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();
|
initialState = currentState();
|
||||||
credentialFields.deviceName.value = device.name;
|
credentialFields.deviceName.value = device.name;
|
||||||
renderDeviceList();
|
renderDeviceList();
|
||||||
|
|||||||
Reference in New Issue
Block a user