feat: OCR auto-saves GPS to asset + MSFS enrichment in detail view

OCR endpoint (/api/ocr) now auto-saves EXIF GPS from the photo
to the asset's latitude/longitude fields when they are blank,
so the first photo scan at a machine permanently records its location.

GET /api/assets/{id} now includes an 'msfs' block enriched from
the merged Dynamics 365 Field Service data (merged-assets.json)
loaded at server start (1,834 matched assets). Fields shown:
equipment_id, connect_id, compass_asset_number, manufacturer,
model_text, dex_model, install_date, software_version, etc.

Frontend updates:
- New 'Field Service Data' card in asset detail view when MSFS
  data is available (manufacturer, Connect ID, install date, etc.)
- '📍 GPS saved to asset' confirmation shown after OCR auto-save
- AppState.ocrGpsSaved flag to track the auto-save event
This commit is contained in:
2026-05-28 22:28:34 -04:00
parent 3a8f89432a
commit a8158566ac
2 changed files with 104 additions and 0 deletions
+61
View File
@@ -43,6 +43,27 @@ DB_PATH = os.environ.get("CANTEEN_DB_PATH", str(Path(__file__).parent / "assets.
UPLOADS_DIR = Path(os.environ.get("CANTEEN_UPLOADS_DIR", str(Path(__file__).parent / "uploads")))
STATIC_DIR = Path(__file__).parent / "static"
# ─── MSFS Data (merged asset enrichment from Dynamics 365 Field Service) ───
MSFS_DATA_PATH = os.environ.get(
"MSFS_DATA_PATH",
str(Path.home() / "projects/ms-field-service-extraction/web/static/data/merged-assets.json"),
)
_MSFS_LOOKUP: dict[str, dict] = {} # keyed by canteen_machine_id
_MSFS_BY_ID: dict[int, dict] = {} # keyed by canteen_id (assets.id)
if os.path.exists(MSFS_DATA_PATH):
try:
with open(MSFS_DATA_PATH, "r") as _f:
_msfs_all = _json.load(_f)
for _entry in _msfs_all.get("assets", []):
_mid = _entry.get("canteen_machine_id")
_cid = _entry.get("canteen_id")
if _mid:
_MSFS_LOOKUP[_mid] = _entry
if _cid:
_MSFS_BY_ID[_cid] = _entry
except Exception:
pass # Non-fatal — MSFS enrichment just won't be available
def _load_categories() -> set:
"""Load valid category names from the categories lookup table."""
try:
@@ -1118,6 +1139,25 @@ def get_asset(asset_id: int):
result["location_name"] = loc["name"] if loc else None
conn.close()
# Enrich with MSFS data (from Dynamics 365 Field Service)
msfs = _MSFS_LOOKUP.get(result.get("machine_id", "")) or _MSFS_BY_ID.get(asset_id)
if msfs:
result["msfs"] = {
"equipment_id": msfs.get("msfs_equipment_id"),
"connect_id": msfs.get("msfs_connect_id"),
"compass_asset_number": msfs.get("msfs_compass_asset_number"),
"manufacturer": msfs.get("msfs_manufacturer"),
"model_text": msfs.get("msfs_model_text"),
"dex_model": msfs.get("msfs_dex_model"),
"install_date": msfs.get("msfs_install_date"),
"manufacture_date": msfs.get("msfs_manufacture_date"),
"software_version": msfs.get("msfs_software_version"),
"canteen_connect_guid": msfs.get("msfs_canteen_connect_guid"),
"account_name": msfs.get("msfs_account_name"),
"work_order_count": msfs.get("work_order_count", 0),
}
return result
@@ -2135,6 +2175,27 @@ async def ocr_sticker(file: UploadFile = File(...), exif_data: str = Form(None))
if exif_gps:
result["exif_gps"] = exif_gps
# Auto-save GPS to asset if lat/lng is blank
machine_id = result.get("machine_id")
if machine_id:
try:
conn = get_db()
asset = conn.execute(
"SELECT id, latitude, longitude FROM assets WHERE machine_id = ?",
(machine_id,),
).fetchone()
if asset and (asset["latitude"] is None or asset["longitude"] is None):
conn.execute(
"UPDATE assets SET latitude = ?, longitude = ?, updated_at = datetime('now') WHERE id = ?",
(exif_gps["lat"], exif_gps["lng"], asset["id"]),
)
conn.commit()
result["gps_saved"] = True
conn.close()
except Exception:
pass # Non-critical — don't fail OCR if GPS save fails
if saved_path:
result["path"] = saved_path
return result