MEGAINFRA-WEBAPP/scripts/migrations/2026-09-22-add-fuel-log-coordinates.py
DEls a589b14ac9 feat(clock): enhance vehicle scan API with GPS support
Updated the vehicle scan API to include optional GPS coordinates (latitude, longitude) and location accuracy. The API now validates these fields, ensuring both latitude and longitude are provided together and within valid ranges. Enhanced error handling for invalid inputs and updated the API reference documentation accordingly. Additionally, modified the FleetScanner component to capture GPS data and reflect its status in the UI.
2026-09-22 19:59:44 +02:00

133 lines
No EOL
4.2 KiB
Python

#!/usr/bin/env python3
"""
One-shot PocketBase migration applied via:
PB_URL=https://pb.mega.infra.apphub.co.za \\
PB_ADMIN_EMAIL=admin@mega.co.za \\
PB_ADMIN_PASSWORD=... \\
python3 scripts/migrations/2026-09-22-add-fuel-log-coordinates.py
Adds `latitude`, `longitude`, `location_accuracy_m` to FUEL_LOGS so the
MEGA Clock Fleet scanner can record the GPS fix at the moment of scan.
The spreadsheet import path doesn't have a GPS context, so
desktop-imported rows stay null — same posture as `latest_consumption`,
which this migration sits next to in the collection's field list.
Range validation (lat ∈ [-90, 90], lng ∈ [-180, 180], accuracy ≥ 0)
is the route's responsibility, not PB's — the field stays permissive
so a future capture device (a hand-held GPS, a survey tool) can write
whatever it reports and a separate cleanup job decides which fixes
are usable.
Idempotent: re-running is safe; existing fields are skipped.
"""
import json
import os
import sys
import urllib.error
import urllib.request
COLLECTION = "fuel_logs"
NEW_FIELDS = [
{
"name": "latitude",
"type": "number",
"required": False,
# No min/max / noDecimal — mirrors `litres` and `odo_reading`.
},
{
"name": "longitude",
"type": "number",
"required": False,
},
{
"name": "location_accuracy_m",
"type": "number",
"required": False,
},
]
def admin_auth(base_url: str) -> str:
email = os.environ["PB_ADMIN_EMAIL"]
password = os.environ["PB_ADMIN_PASSWORD"]
body = json.dumps({"identity": email, "password": password}).encode()
req = urllib.request.Request(
f"{base_url}/api/collections/_superusers/auth-with-password",
data=body,
headers={"Content-Type": "application/json"},
method="POST",
)
with urllib.request.urlopen(req) as resp:
return json.loads(resp.read())["token"]
def get_collection(base_url: str, token: str, name: str):
req = urllib.request.Request(
f"{base_url}/api/collections/{name}",
headers={"Authorization": token},
)
try:
with urllib.request.urlopen(req) as resp:
return json.loads(resp.read())
except urllib.error.HTTPError as e:
if e.code == 404:
return None
raise
def patch_collection(base_url: str, token: str, name: str, body: dict):
req = urllib.request.Request(
f"{base_url}/api/collections/{name}",
data=json.dumps(body).encode(),
headers={
"Authorization": token,
"Content-Type": "application/json",
},
method="PATCH",
)
try:
with urllib.request.urlopen(req) as resp:
return json.loads(resp.read())
except urllib.error.HTTPError as e:
body_txt = e.read().decode() if e.fp else ""
raise RuntimeError(f"PATCH {name} failed: {e.code} {body_txt}") from e
def main() -> int:
base_url = os.environ.get("PB_URL", "http://127.0.0.1:8090").rstrip("/")
token = admin_auth(base_url)
coll = get_collection(base_url, token, COLLECTION)
if not coll:
print(f"ERROR: collection {COLLECTION!r} not found on {base_url}")
return 1
existing_fields = coll.get("fields", [])
existing_field_names = {f["name"] for f in existing_fields}
new_fields = [f for f in NEW_FIELDS if f["name"] not in existing_field_names]
if new_fields:
merged_fields = existing_fields + new_fields
print(f"Adding {len(new_fields)} new field(s) to {COLLECTION}: "
f"{[f['name'] for f in new_fields]}")
patch_collection(base_url, token, COLLECTION, {"fields": merged_fields})
else:
print(f"No-op: all NEW_FIELDS already present on {COLLECTION}.")
final = get_collection(base_url, token, COLLECTION)
final_field_names = sorted(f["name"] for f in final.get("fields", []))
print(f"\nFinal field list on {COLLECTION}: {final_field_names}")
print("\nMigration complete.")
return 0
if __name__ == "__main__":
try:
sys.exit(main())
except KeyError as e:
print(f"ERROR: missing environment variable {e}. "
"Set PB_ADMIN_EMAIL and PB_ADMIN_PASSWORD.", file=sys.stderr)
sys.exit(2)