From e493f0f51aa674dea4655685bc84962562de6d38 Mon Sep 17 00:00:00 2001 From: Shawn Date: Tue, 28 Jul 2026 23:31:52 -0400 Subject: [PATCH] Fix syntax errors: fix duplicate execute() call indentation and f-string continuation issue --- server.py | 216 +++++++++++++++++++++++++++--------------------------- 1 file changed, 106 insertions(+), 110 deletions(-) diff --git a/server.py b/server.py index 8fabfcf..e3576b0 100644 --- a/server.py +++ b/server.py @@ -1,5 +1,5 @@ """ -Canteen Asset Geolocation Tool — FastAPI server. +Canteen Asset Geolocation Tool - FastAPI server. Single-file backend: SQLite storage, asset CRUD, machine_id search, check-ins with GPS, stats, and CSV export. @@ -47,7 +47,7 @@ except Exception: import piexif from PIL import Image as PILImage -# ─── Asset matcher (photo OCR → DB lookup) ───────────────────────────────── +# --- Asset matcher (photo OCR -> DB lookup) --------------------------------- from classify_makes import normalize_identifier, find_asset_by_normalized_id from fastapi import FastAPI, HTTPException, Query, Request, UploadFile, File, Form, Response @@ -57,13 +57,13 @@ from fastapi.staticfiles import StaticFiles from pydantic import BaseModel from typing import Optional, List -# ─── Config ───────────────────────────────────────────────────────────────── +# --- Config ----------------------------------------------------------------- DB_PATH = os.environ.get("CANTEEN_DB_PATH", str(Path(__file__).parent / "assets.db")) 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 (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"), @@ -82,7 +82,7 @@ if os.path.exists(MSFS_DATA_PATH): if _cid: _MSFS_BY_ID[_cid] = _entry except Exception: - pass # Non-fatal — MSFS enrichment just won't be available + pass # Non-fatal - MSFS enrichment just won't be available def _load_categories() -> set: """Load valid category names from the categories lookup table.""" @@ -98,7 +98,7 @@ VALID_CATEGORIES = _load_categories() VALID_STATUSES = {"active", "maintenance", "retired"} -# ─── Database ─────────────────────────────────────────────────────────────── +# --- Database --------------------------------------------------------------- def get_db() -> sqlite3.Connection: @@ -466,7 +466,7 @@ def _migrate_v1_to_v2(conn: sqlite3.Connection): def init_db(conn: sqlite3.Connection): - """Create tables and indexes if they don't exist. Runs v1→v2 migration if needed.""" + """Create tables and indexes if they don't exist. Runs v1->v2 migration if needed.""" # Check if assets table exists and has old schema cursor = conn.execute( "SELECT name FROM sqlite_master WHERE type='table' AND name='assets'" @@ -478,11 +478,11 @@ def init_db(conn: sqlite3.Connection): _migrate_v1_to_v2(conn) return - # Fresh install or already migrated — create all tables + # Fresh install or already migrated - create all tables _create_v2_tables(conn) _seed_data(conn) _migrate_sessions_expires_at(conn) - # Asset indexes — created here (not in _create_v2_tables) to avoid + # Asset indexes - created here (not in _create_v2_tables) to avoid # failing during migration when old v1 assets table lacks machine_id. _ensure_unique_machine_id(conn) conn.execute( @@ -529,7 +529,7 @@ def init_db(conn: sqlite3.Connection): se_cols = conn.execute("PRAGMA table_info(service_entrances)").fetchall() se_name_col = next((c for c in se_cols if c[1] == 'name'), None) if se_name_col and not se_name_col[3]: # notnull == 0 - # Table exists but with wrong schema — recreate it (new table, safe to drop) + # Table exists but with wrong schema - recreate it (new table, safe to drop) conn.execute("DROP TABLE service_entrances") se_exists = False @@ -549,7 +549,7 @@ def init_db(conn: sqlite3.Connection): conn.commit() -# ─── App / Middleware ─────────────────────────────────────────────────────── +# --- App / Middleware ------------------------------------------------------- @asynccontextmanager @@ -571,7 +571,7 @@ app.add_middleware( allow_headers=["*"], ) -# ─── Auth Middleware ────────────────────────────────────────────────────────── +# --- Auth Middleware ---------------------------------------------------------- @app.middleware("http") @@ -584,7 +584,7 @@ async def auth_middleware(request: Request, call_next): if os.environ.get("CANTEEN_SKIP_AUTH") == "1": return await call_next(request) - # Public paths — no auth required + # Public paths - no auth required if not path.startswith("/api/") or path == "/api/auth/login": return await call_next(request) @@ -607,7 +607,7 @@ async def auth_middleware(request: Request, call_next): } return await call_next(request) - # No valid token — use default admin user (field tool mode) + # No valid token - use default admin user (field tool mode) request.state.current_user = { "id": 1, "username": "admin", @@ -616,7 +616,7 @@ async def auth_middleware(request: Request, call_next): return await call_next(request) -# ─── Global Error Handling ─────────────────────────────────────────────────── +# --- Global Error Handling --------------------------------------------------- @app.exception_handler(HTTPException) @@ -630,7 +630,7 @@ async def http_exception_handler(request: Request, exc: HTTPException): @app.exception_handler(Exception) async def generic_exception_handler(request: Request, exc: Exception): - """Catch-all for unhandled exceptions — log and return 500.""" + """Catch-all for unhandled exceptions - log and return 500.""" import traceback traceback.print_exc() return JSONResponse( @@ -639,7 +639,7 @@ async def generic_exception_handler(request: Request, exc: Exception): ) -# ─── Input sanitization helpers ────────────────────────────────────────────── +# --- Input sanitization helpers ---------------------------------------------- def _sanitize_machine_id(machine_id: str) -> str: @@ -662,7 +662,7 @@ def _sanitize_name(name: str) -> str: return clean -# ─── Pydantic Models ──────────────────────────────────────────────────────── +# --- Pydantic Models -------------------------------------------------------- class AssetKey(BaseModel): @@ -763,7 +763,7 @@ class VisitCreate(BaseModel): -# ─── Helpers ──────────────────────────────────────────────────────────────── +# --- Helpers ---------------------------------------------------------------- def row_to_dict(row: sqlite3.Row) -> dict: @@ -801,7 +801,7 @@ def _sync_geofence_users(conn: sqlite3.Connection, geofence_id: int, user_ids: l """Replace assigned users for a geofence with the given list of user IDs. Validates all user_ids exist before modifying the junction table. - Does NOT commit — caller manages transaction boundaries. + Does NOT commit - caller manages transaction boundaries. """ # Validate all user IDs exist if user_ids: @@ -863,7 +863,7 @@ def _validate_enum_table(conn: sqlite3.Connection, table: str, value: str, label ) -# ─── Task 3: Health ──────────────────────────────────────────────────────── +# --- Task 3: Health -------------------------------------------------------- @app.get("/health") @@ -871,7 +871,7 @@ def health(): return {"status": "ok"} -# ─── Customer & Location listing (for filter dropdowns) ───────────────────── +# --- Customer & Location listing (for filter dropdowns) --------------------- @app.get("/api/customers") @@ -940,7 +940,7 @@ def list_places(): return [r["place"] for r in rows] -# ─── Task 4: POST /api/assets ────────────────────────────────────────────── +# --- Task 4: POST /api/assets ---------------------------------------------- def _build_asset_insert(body: AssetCreate, machine_id: str, name: str): @@ -1061,7 +1061,7 @@ def create_asset(body: AssetCreate): return result -# ─── Task 5: GET /api/assets ─────────────────────────────────────────────── +# --- Task 5: GET /api/assets ----------------------------------------------- @app.get("/api/assets") @@ -1162,7 +1162,7 @@ def list_assets( ) params.extend(matching_prefixes) else: - # Branch name not found — return empty result + # Branch name not found - return empty result conditions.append("1=0") where = " AND ".join(conditions) @@ -1212,14 +1212,14 @@ def search_by_machine_id(machine_id: str = Query(...)): return row_to_dict(row) -# ── Task 5b: GET /api/branches ───────────────────────────────────────────── +# -- Task 5b: GET /api/branches --------------------------------------------- @app.get("/api/branches") def list_branches(): """Return distinct branch/territory names that assets belong to. - Uses the connect_id prefix → branch mapping from the extraction DB. + Uses the connect_id prefix -> branch mapping from the extraction DB. Returns sorted list of {name, machine_count} objects. """ cache = _build_asset_branch_cache() @@ -1365,32 +1365,31 @@ def get_asset(asset_id: int): if asset_company: # Try exact match first, then LIKE cur_ext.execute(""" - cur_ext.execute(""" - SELECT name, address1_line1, address1_city, address1_stateorprovince, - address1_postalcode, address1_country, telephone1, emailaddress1, - websiteurl, fax, address1_latitude, address1_longitude, - "hsl_managementcompany!name" AS management_company, - "hsl_csm!name" AS csm, - "parentaccountid!name" AS parent_company, - "msdyn_billingaccount!name" AS billing_account - FROM account - WHERE name = ? - """, (asset_company,)) - acct = cur_ext.fetchone() - if not acct: - cur_ext.execute(""" - SELECT name, address1_line1, address1_city, address1_stateorprovince, - address1_postalcode, address1_country, telephone1, emailaddress1, - websiteurl, fax, address1_latitude, address1_longitude, - "hsl_managementcompany!name" AS management_company, - "hsl_csm!name" AS csm, - "parentaccountid!name" AS parent_company, - "msdyn_billingaccount!name" AS billing_account - FROM account - WHERE name LIKE ? LIMIT 1 - """, (f"%{asset_company[:30]}%",)) - acct = cur_ext.fetchone() - if acct: + SELECT name, address1_line1, address1_city, address1_stateorprovince, + address1_postalcode, address1_country, telephone1, emailaddress1, + websiteurl, fax, address1_latitude, address1_longitude, + "hsl_managementcompany!name" AS management_company, + "hsl_csm!name" AS csm, + "parentaccountid!name" AS parent_company, + "msdyn_billingaccount!name" AS billing_account + FROM account + WHERE name = ? + """, (asset_company,)) + acct = cur_ext.fetchone() + if not acct: + cur_ext.execute(""" + SELECT name, address1_line1, address1_city, address1_stateorprovince, + address1_postalcode, address1_country, telephone1, emailaddress1, + websiteurl, fax, address1_latitude, address1_longitude, + "hsl_managementcompany!name" AS management_company, + "hsl_csm!name" AS csm, + "parentaccountid!name" AS parent_company, + "msdyn_billingaccount!name" AS billing_account + FROM account + WHERE name LIKE ? LIMIT 1 + """, (f"%{asset_company[:30]}%",)) + acct = cur_ext.fetchone() + if acct: ca = dict(acct) # Get linked companies (sibling accounts under same parent) parent_name = ca.get("parent_company") @@ -1425,7 +1424,7 @@ def get_asset(asset_id: int): return result -# ─── Navigation endpoint ──────────────────────────────────────────────────── +# --- Navigation endpoint ---------------------------------------------------- @app.get("/api/assets/{asset_id}/navigation") def get_navigation( @@ -1460,7 +1459,7 @@ def get_navigation( dest_lat, dest_lng = row["latitude"], row["longitude"] - # ── Try OSRM first ────────────────────────────────────────────────── + # -- Try OSRM first -------------------------------------------------- osrm_profile = "driving" if mode == "driving" else "foot" route_coords = None route_duration_s = None @@ -1497,10 +1496,10 @@ def get_navigation( if steps: walking_directions = steps except Exception: - # OSRM failed — fall through to Haversine below + # OSRM failed - fall through to Haversine below pass - # ── Haversine distance (always computed for fallback / reference) ──── + # -- Haversine distance (always computed for fallback / reference) ---- R = 6371000 # Earth radius in meters phi1 = math.radians(lat) phi2 = math.radians(dest_lat) @@ -1557,7 +1556,7 @@ def get_navigation( return result -# ─── Task 6: PUT / DELETE /api/assets/{id} ───────────────────────────────── +# --- Task 6: PUT / DELETE /api/assets/{id} --------------------------------- _TEXT_FIELDS = [ @@ -1661,7 +1660,7 @@ def delete_asset(asset_id: int): conn.close() -# ─── Task 8: POST /api/checkins ───────────────────────────────────────────── +# --- Task 8: POST /api/checkins --------------------------------------------- @app.post("/api/checkins", status_code=201) @@ -1704,7 +1703,7 @@ def create_checkin(body: CheckinCreate, request: Request): row = conn.execute("SELECT created_at FROM checkins WHERE id = ?", (checkin_id,)).fetchone() _auto_log_visit(conn, user_id, body.asset_id, row["created_at"]) - # Activity log — include machine_id and name for clarity + # Activity log - include machine_id and name for clarity asset_info = conn.execute( "SELECT machine_id, name FROM assets WHERE id = ?", (body.asset_id,) @@ -1727,7 +1726,7 @@ def create_checkin(body: CheckinCreate, request: Request): return row_to_dict(row) -# ─── Task 9: GET /api/checkins ────────────────────────────────────────────── +# --- Task 9: GET /api/checkins ---------------------------------------------- @app.get("/api/checkins") @@ -1811,7 +1810,7 @@ def delete_checkin(checkin_id: int): conn.close() -# ─── Task 10: GET /api/stats ──────────────────────────────────────────────── +# --- Task 10: GET /api/stats ------------------------------------------------ @@ -1823,7 +1822,7 @@ def delete_checkin(checkin_id: int): -# ─── Phase C: Helpers ──────────────────────────────────────────────────────── +# --- Phase C: Helpers -------------------------------------------------------- VALID_ROLES = {"admin", "technician", "readonly"} @@ -1860,7 +1859,7 @@ class LoginRequest(BaseModel): password: str remember_me: bool = False -# ─── Phase C: Auth API ─────────────────────────────────────────────────────── +# --- Phase C: Auth API ------------------------------------------------------- @app.post("/api/auth/login") @@ -1941,7 +1940,7 @@ def logout(request: Request): -# ─── Phase 0: Proximity Check ───────────────────────────────────────────────── +# --- Phase 0: Proximity Check ------------------------------------------------- @app.get("/api/proximity") @@ -1976,7 +1975,7 @@ def proximity_check( return results -# ─── Phase C: Visits API & Auto-visit Logging ──────────────────────────────── +# --- Phase C: Visits API & Auto-visit Logging -------------------------------- def _auto_log_visit(conn: sqlite3.Connection, user_id: int, asset_id: int, @@ -1993,7 +1992,7 @@ def _auto_log_visit(conn: sqlite3.Connection, user_id: int, asset_id: int, ).fetchone() if prev is None: - # No prior visit — check if there are at least 2 check-ins in the window + # No prior visit - check if there are at least 2 check-ins in the window rows = conn.execute( """SELECT id, created_at FROM checkins WHERE user_id = ? AND asset_id = ? @@ -2129,7 +2128,7 @@ def get_visit_stats(): -# ─── File Uploads ─────────────────────────────────────────────────────────── +# --- File Uploads ----------------------------------------------------------- ICON_MAX_SIZE = 2 * 1024 * 1024 # 2 MB PHOTO_MAX_SIZE = 20 * 1024 * 1024 # 20 MB @@ -2244,7 +2243,7 @@ def _save_upload_bytes(contents: bytes, filename: str | None, subdir: str, allow def _re_embed_exif(filepath: Path, exif_json: str): """Re-embed EXIF data (from client-side exifr.parse) into a saved JPEG. - This is a defense against EXIF being stripped during upload — the client + This is a defense against EXIF being stripped during upload - the client reads EXIF from the original file before sending, and we write it back. """ try: @@ -2413,7 +2412,7 @@ async def ocr_sticker(file: UploadFile = File(...), exif_data: str = Form(None)) except Exception: saved_path = None # fall through to temp-file path below - # Save file for OCR — use permanent path if available, otherwise temp + # Save file for OCR - use permanent path if available, otherwise temp if saved_path: ocr_path = UPLOADS_DIR / saved_path.split("/", 2)[-1] else: @@ -2429,7 +2428,7 @@ async def ocr_sticker(file: UploadFile = File(...), exif_data: str = Form(None)) ocr_source = "none" ollama_text = "" - # Try Ollama vision model first (Windows PC) — most accurate + # Try Ollama vision model first (Windows PC) - most accurate if _HAS_OLLAMA: try: import urllib.request as _ourl @@ -2496,7 +2495,7 @@ async def ocr_sticker(file: UploadFile = File(...), exif_data: str = Form(None)) detail = "No OCR service available (Tesseract not installed, Ollama not connected)." raise HTTPException(status_code=422, detail=detail) - # Build response — search for identifiers in the OCR text + # Build response - search for identifiers in the OCR text result: dict = { "raw_text": text.strip()[:1000], "ocr_source": ocr_source, @@ -2524,7 +2523,7 @@ async def ocr_sticker(file: UploadFile = File(...), exif_data: str = Form(None)) result["confidence"] = "none" result["detail"] = "No machine ID pattern found in image. Try again with better lighting." - # 2. Cross-reference OCR text against DB — find matched assets by + # 2. Cross-reference OCR text against DB - find matched assets by # serial_number, connect_id, equipment_id, or barcode db_path = DB_PATH db_matches = [] @@ -2591,7 +2590,7 @@ async def ocr_sticker(file: UploadFile = File(...), exif_data: str = Form(None)) if updated_count > 0: result["photo_saved"] = updated_count except Exception: - pass # Non-critical — don't fail OCR if photo save fails + pass # Non-critical - don't fail OCR if photo save fails # Auto-update serial_number on matched assets from OCR text if db_matches and any(m.get("serial_number") == "" for m in db_matches): @@ -2635,14 +2634,14 @@ async def ocr_sticker(file: UploadFile = File(...), exif_data: str = Form(None)) result["gps_saved"] = True conn.close() except Exception: - pass # Non-critical — don't fail OCR if GPS save fails + pass # Non-critical - don't fail OCR if GPS save fails if saved_path: result["path"] = saved_path return result -# ─── Match raw text against DB (for barcode scanner / client-side OCR) ──── +# --- Match raw text against DB (for barcode scanner / client-side OCR) ---- @app.post("/api/match-text", status_code=200) async def match_text(text: str = Form(...)): @@ -2711,7 +2710,7 @@ async def match_text(text: str = Form(...)): } -# ─── T4: Connect Label — unified photo + OCR + GPS endpoint ───────────────── +# --- T4: Connect Label - unified photo + OCR + GPS endpoint ----------------- class ConnectLabelRequest(BaseModel): """Request body for the connect-label endpoint.""" @@ -2738,7 +2737,7 @@ async def connect_label( Accepts multipart form data with optional photo upload. Creates an asset with the provided machine_id, name, GPS coords. If photo is uploaded, it saves the file and sets photo_path. - Validates machine_id format (XXXXX-XXXXXX → last 5 digits). + Validates machine_id format (XXXXX-XXXXXX -> last 5 digits). """ # If machine_id/name come as form fields, use those; otherwise try query params machine_id = _sanitize_machine_id(machine_id) @@ -2812,13 +2811,13 @@ async def connect_label( return result -# ─── Reverse Geocode (Nominatim) ──────────────────────────────────────────── +# --- Reverse Geocode (Nominatim) -------------------------------------------- def reverse_geocode(lat: float, lng: float) -> dict | None: """Call OpenStreetMap Nominatim to reverse geocode GPS coords. Returns a dict with address fields mapped to our schema, or None on failure. - Does NOT raise HTTPException — callers handle the None case. + Does NOT raise HTTPException - callers handle the None case. """ try: url = ( @@ -2879,7 +2878,7 @@ def geocode(lat: float = Query(...), lng: float = Query(...)): return result -# ─── Disney Park Classification ────────────────────────────────────────────── +# --- Disney Park Classification ---------------------------------------------- @app.get("/api/disney/stats") @@ -2934,7 +2933,7 @@ def set_asset_disney_park(asset_id: int, body: dict): return {"status": "ok", "asset_id": asset_id, "disney_park": park} -# ─── Service Entrances ──────────────────────────────────────────────────────── +# --- Service Entrances -------------------------------------------------------- class ServiceEntranceCreate(BaseModel): @@ -3051,10 +3050,10 @@ def list_all_service_entrances(): # ═══════════════════════════════════════════════════════════════════════════ -# WORK ORDER & ROUTE OPTIMIZATION — reads extraction DB directly +# WORK ORDER & ROUTE OPTIMIZATION - reads extraction DB directly # ═══════════════════════════════════════════════════════════════════════════ -# ── Extraction DB (MS Field Service sync) ──────────────────────────────── +# -- Extraction DB (MS Field Service sync) -------------------------------- EXTRACTION_PROJECT = Path.home() / "projects" / "ms-field-service-extraction" EXTRACTION_DB = ( EXTRACTION_PROJECT @@ -3088,7 +3087,7 @@ def _get_extraction_db() -> sqlite3.Connection | None: return conn -# ── Asset Branch Cache (connect_id prefix → branch/territory name) ──────── +# -- Asset Branch Cache (connect_id prefix -> branch/territory name) -------- _BRANCH_CACHE: dict[str, str] | None = None @@ -3106,7 +3105,7 @@ def _build_asset_branch_cache() -> dict[str, str]: result: dict[str, str] = {} conn = _get_extraction_db() if not conn: - print("⚠️ Extraction DB not available — branch cache will be empty") + print("⚠️ Extraction DB not available - branch cache will be empty") _BRANCH_CACHE = result return result @@ -3153,7 +3152,7 @@ def _build_asset_branch_cache() -> dict[str, str]: return result -# ── TSP Solver ─────────────────────────────────────────────────────────── +# -- TSP Solver ----------------------------------------------------------- def _haversine(lat1, lon1, lat2, lon2): @@ -3213,7 +3212,7 @@ def _solve_tsp(points, origin_idx=0): return route -# ── Work Order Search ────────────────────────────────────────────────────── +# -- Work Order Search ------------------------------------------------------ @app.get("/api/workorders/search") @@ -3228,7 +3227,7 @@ async def workorders_search( """Search work orders by name, account, or city, with optional status and date filters.""" conn = _get_extraction_db() if not conn: - raise HTTPException(503, "Database not available — sync extraction DB missing") + raise HTTPException(503, "Database not available - sync extraction DB missing") try: cur = conn.cursor() params: list = [] @@ -3240,7 +3239,7 @@ async def workorders_search( where_clauses.append("(w.msdyn_name LIKE ? OR a.name LIKE ? OR a.address1_city LIKE ?)") params.extend([like, like, like]) - # Status filter — map label back to code + # Status filter - map label back to code if status: status_label_map = {v: k for k, v in STATUS_LABELS.items()} code = status_label_map.get(status) @@ -3328,7 +3327,7 @@ async def workorders_search( conn.close() -# ── Work Order Lookup (by ID) ────────────────────────────────────────────── +# -- Work Order Lookup (by ID) ---------------------------------------------- class WorkorderLookupRequest(BaseModel): @@ -3346,7 +3345,7 @@ async def workorders_lookup(body: WorkorderLookupRequest): conn = _get_extraction_db() if not conn: - raise HTTPException(503, "Database not available — sync extraction DB missing") + raise HTTPException(503, "Database not available - sync extraction DB missing") try: cur = conn.cursor() placeholders = ",".join(["?"] * len(ids)) @@ -3510,7 +3509,7 @@ async def workorders_lookup(body: WorkorderLookupRequest): conn.close() -# ── Today's Active Work Orders ────────────────────────────────────────────── +# -- Today's Active Work Orders ---------------------------------------------- @app.get("/api/workorders/today") @@ -3522,7 +3521,7 @@ async def workorders_today( work_type: str = Query("", description="Filter by work type (Install/Repair/PM/Emergency)"), date_range: str = Query("today", description="Date range: today, week, month, all"), ): - """Fetch today's active work orders via bookableresourcebooking. + """Fetch todays active work orders via bookableresourcebooking. Pass ?tech=Shawn+Canada&tech=John+Doe to filter by one or more technicians. Omit tech entirely to return all technicians. @@ -3530,7 +3529,7 @@ async def workorders_today( """ conn = _get_extraction_db() if not conn: - raise HTTPException(503, "Database not available — sync extraction DB missing") + raise HTTPException(503, "Database not available - sync extraction DB missing") try: cur = conn.cursor() today = date.today().isoformat() @@ -3661,7 +3660,7 @@ async def workorders_today( conn.close() -# ── List Technicians ────────────────────────────────────────────────────── +# -- List Technicians ------------------------------------------------------ @app.get("/api/workorders/technicians") @@ -3669,7 +3668,7 @@ async def workorders_technicians(): """Return distinct technician names from bookings.""" conn = _get_extraction_db() if not conn: - raise HTTPException(503, "Database not available — sync extraction DB missing") + raise HTTPException(503, "Database not available - sync extraction DB missing") try: cur = conn.cursor() cur.execute( @@ -3694,7 +3693,7 @@ async def workorders_technicians(): conn.close() -# ── Work Order List (filterable, paginated) ───────────────────────────────── +# -- Work Order List (filterable, paginated) --------------------------------- @app.get("/api/workorders/list") @@ -3716,7 +3715,7 @@ async def workorders_list( """ conn = _get_extraction_db() if not conn: - raise HTTPException(503, "Database not available — sync extraction DB missing") + raise HTTPException(503, "Database not available - sync extraction DB missing") try: cur = conn.cursor() params: list = [] @@ -3744,7 +3743,7 @@ async def workorders_list( where_clauses.append(f"w.msdyn_systemstatus IN ({placeholders})") params.extend(status_codes) - # Technician filter — check bookings table AND hsl_bookedresource fields + # Technician filter - check bookings table AND hsl_bookedresource fields if tech: tech_names = [t.strip() for t in tech.split(",") if t.strip()] if tech_names: @@ -3760,7 +3759,7 @@ async def workorders_list( where_clauses.append("(" + " OR ".join(tech_conditions) + ")") params.extend(tech_names * 4) - # Location filter — matches city, functional location, or account name + # Location filter - matches city, functional location, or account name if location: like = f"%{location}%" where_clauses.append( @@ -3903,7 +3902,7 @@ async def workorders_list( conn.close() -# ── Route Optimization ────────────────────────────────────────────────────── +# -- Route Optimization ------------------------------------------------------ def _lookup_asset_gps_by_machine_id(machine_id: str) -> dict | None: """Lookup GPS from the canteen-asset-tracker assets table by machine_id.""" @@ -3942,12 +3941,11 @@ async def route_optimize(body: RouteOptimizeRequest): conn = _get_extraction_db() if not conn: - raise HTTPException(503, "Database not available — sync extraction DB missing") + raise HTTPException(503, "Database not available - sync extraction DB missing") try: cur = conn.cursor() placeholders = ",".join(["?"] * len(wo_ids)) - cur.execute( - f""" + cur.execute(f""" SELECT w.msdyn_workorderid, w.msdyn_name, w."msdyn_serviceaccount!name" AS account_name, @@ -3972,9 +3970,7 @@ async def route_optimize(body: RouteOptimizeRequest): LEFT JOIN msdyn_customerasset ca ON w."msdyn_customerasset!id" = ca.msdyn_customerassetid WHERE w.msdyn_name IN ({placeholders}) OR w.msdyn_workorderid IN ({placeholders}) - """, - wo_ids + wo_ids, - ) + """, wo_ids + wo_ids) rows = cur.fetchall() finally: conn.close() @@ -3994,7 +3990,7 @@ async def route_optimize(body: RouteOptimizeRequest): except (ValueError, TypeError): pass - # Machine GPS — prefer msdyn_customerasset GPS, fall back to canteen assets table + # Machine GPS - prefer msdyn_customerasset GPS, fall back to canteen assets table machine_gps = None asset_lat = r["asset_lat"] asset_lng = r["asset_lng"] @@ -4117,7 +4113,7 @@ async def route_optimize(body: RouteOptimizeRequest): return result -# ─── Static Files (mounted last to not shadow routes) ────────────────────── +# --- Static Files (mounted last to not shadow routes) ---------------------- app.mount("/uploads", StaticFiles(directory=str(UPLOADS_DIR)), name="uploads")