|
- import asyncio
- import json
- import os
- import tempfile
- from urllib.parse import urlencode
-
- from fastapi import APIRouter, Depends, File, Form, HTTPException, Query, Request, UploadFile
- import httpx
- import config_env
- from typing import Any, Dict, List, Optional
-
- from schemas.reslevis import (
- BuildingItem,
- FloorItem,
- ZoneItem,
- ZoneAreaDefinitionItem,
- GatewayItem,
- TrackerItem,
- OperatorItem,
- SubjectItem,
- AlarmItem,
- AlarmCoreItem,
- AlarmStatusUpdateItem,
- TrackItem,
- TrackHistoryItem,
- TrackerZoneItem,
- SettingItem,
- GuiConfigItem,
- UserPreferencesItem,
- UserPreferencesUpdateItem,
- FloorMapUploadResponseItem,
- CoreSettingsItem,
- CoreSettingsUpdateItem,
- )
-
- from logica_reslevis.gateway import GatewayJsonRepository
- from logica_reslevis.building import BuildingJsonRepository
- from logica_reslevis.floor import FloorJsonRepository
- from logica_reslevis.zone import ZoneJsonRepository
- from logica_reslevis.zone_area_definition import ZoneAreaDefinitionJsonRepository
- from logica_reslevis.tracker import TrackerJsonRepository
- from logica_reslevis.operator import OperatorJsonRepository
- from logica_reslevis.setting import SettingJsonRepository
- from logica_reslevis.gui_config import GuiConfigJsonRepository
- from logica_reslevis.user_preferences import UserPreferencesJsonRepository
- from logica_reslevis.subject import SubjectJsonRepository
- from logica_reslevis.alarm import AlarmJsonRepository
- from logica_reslevis.track import TrackJsonRepository
- from logica_reslevis.tracker_zone import TrackerZoneJsonRepository
- from logica_reslevis.tracker_mode import get_mode_aware_trackers
-
- from security import get_current_user
-
- #CORE SYNC
- CORE_BASE_URL = config_env.CORE_API_URL.rstrip("/")
- ALERTS_CORE_BASE_URL = "http://localhost:1902"
- TRACKS_CORE_BASE_URL = "http://localhost:1902"
- SETTINGS_CORE_BASE_URL = "http://127.0.0.1:1902"
- CORE_TIMEOUT = 2.0 # secondi
- PNG_SIGNATURE = b"\x89PNG\r\n\x1a\n"
-
- async def sync_core_get(request: Request) -> None:
- if request.method != "GET":
- return
-
- sync = CORE_GET_SYNC.get(request.url.path)
- if sync is None:
- return
- repo, normalizer = sync
-
- try:
- async with httpx.AsyncClient(timeout=CORE_TIMEOUT) as client:
- resp = await client.get(
- f"{CORE_BASE_URL}{request.url.path}",
- params=request.query_params,
- )
-
- if 200 <= resp.status_code < 300:
- data = resp.json()
- if isinstance(data, list):
- if normalizer:
- data = [normalizer(r) for r in data if isinstance(r, dict)]
- repo._write_all(data) # aggiorna i file locali
- except (httpx.RequestError, ValueError):
- # CORE giù o risposta non valida -> uso il file locale
- pass
-
-
- router = APIRouter(dependencies=[Depends(sync_core_get)])
-
- gateway_repo = GatewayJsonRepository()
- building_repo = BuildingJsonRepository()
- floor_repo = FloorJsonRepository()
- zone_repo = ZoneJsonRepository()
- zone_area_definition_repo = ZoneAreaDefinitionJsonRepository()
- tracker_repo = TrackerJsonRepository()
- operator_repo = OperatorJsonRepository()
- subject_repo = SubjectJsonRepository()
- alarm_repo = AlarmJsonRepository()
- track_repo = TrackJsonRepository()
- tracker_zone_repo = TrackerZoneJsonRepository()
- setting_repo = SettingJsonRepository()
- gui_config_repo = GuiConfigJsonRepository()
- user_preferences_repo = UserPreferencesJsonRepository()
-
- def _none_if_empty(v):
- return None if v in ("", None, 0, "0") else v
-
- def _str_or_none(v):
- if v in ("", None):
- return None
- if isinstance(v, (int, float, bool)):
- return str(v)
- return v
-
- def _uuid_list(values):
- if values in ("", None):
- return []
- if isinstance(values, str):
- values = [v for v in values.split(",") if v]
- if isinstance(values, (list, tuple, set)):
- cleaned = []
- for v in values:
- if isinstance(v, dict):
- v = v.get("id") or v.get("uuid")
- if v in ("", None, 0, "0"):
- continue
- cleaned.append(v)
- return cleaned
- return [values] if values not in ("", None, 0, "0") else []
-
- def _normalize_gateway(row: dict) -> dict:
- row = dict(row)
- row["floor"] = _none_if_empty(row.get("floor"))
- row["building"] = _none_if_empty(row.get("building"))
- return row
-
- def _normalize_track(row: dict) -> dict:
- row = dict(row)
- row["ID"] = row.get("ID")
- row["gateway"] = _none_if_empty(row.get("gateway"))
- row["tracker"] = _none_if_empty(row.get("tracker"))
- row["subject"] = _none_if_empty(row.get("subject"))
- row["floor"] = _none_if_empty(row.get("floor"))
- row["building"] = _none_if_empty(row.get("building"))
- row["timestamp"] = _str_or_none(row.get("timestamp"))
- row["type"] = _str_or_none(row.get("type"))
- row["status"] = _str_or_none(row.get("status"))
- row["gatewayMac"] = _str_or_none(row.get("gatewayMac"))
- row["trackerMac"] = _str_or_none(row.get("trackerMac"))
- row["subjectName"] = _str_or_none(row.get("subjectName"))
- row["x"] = None if row.get("x") in ("", None) else row.get("x")
- row["y"] = None if row.get("y") in ("", None) else row.get("y")
- row["z"] = None if row.get("z") in ("", None) else row.get("z")
- # signal resta float o None
- row["signal"] = None if row.get("signal") in ("", None) else row.get("signal")
- return row
-
- def _normalize_zone(row: dict) -> dict:
- row = dict(row)
- row["floor"] = _none_if_empty(row.get("floor"))
- row["building"] = _none_if_empty(row.get("building"))
- row["groups"] = _uuid_list(row.get("groups"))
- return row
-
-
- def _uid_from_claims(claims: dict) -> str:
- uid = (
- claims.get("preferred_username")
- or claims.get("username")
- or claims.get("email")
- or claims.get("sub")
- )
- if not uid:
- raise HTTPException(status_code=401, detail="User identity not found in token")
- return str(uid)
-
-
- def _floor_maps_index_path() -> str:
- return os.path.join(config_env.RESLEVIS_MAPS_DIR, "maps.json")
-
-
- def _read_floor_maps_index() -> Dict[str, Any]:
- path = _floor_maps_index_path()
- if not os.path.isfile(path):
- return {"items": [], "count": 0}
-
- try:
- with open(path, "r", encoding="utf-8") as fp:
- data = json.load(fp)
- except (OSError, ValueError):
- return {"items": [], "count": 0}
-
- if not isinstance(data, dict):
- return {"items": [], "count": 0}
-
- items = data.get("items")
- if not isinstance(items, list):
- items = []
- return {"items": items, "count": len(items)}
-
-
- def _write_floor_maps_index(index: Dict[str, Any]) -> None:
- maps_dir = config_env.RESLEVIS_MAPS_DIR
- os.makedirs(maps_dir, exist_ok=True)
-
- index["count"] = len(index.get("items") or [])
- payload = json.dumps(index, ensure_ascii=False, indent=2)
-
- temp_name = None
- try:
- with tempfile.NamedTemporaryFile("w", dir=maps_dir, delete=False, encoding="utf-8") as tmp:
- tmp.write(payload)
- tmp.flush()
- os.fsync(tmp.fileno())
- temp_name = tmp.name
-
- os.replace(temp_name, _floor_maps_index_path())
- try:
- os.chmod(_floor_maps_index_path(), 0o664)
- except OSError:
- pass
- except OSError as exc:
- if temp_name and os.path.exists(temp_name):
- try:
- os.remove(temp_name)
- except OSError:
- pass
- raise HTTPException(status_code=500, detail="Unable to update maps.json") from exc
-
-
- def _same_floor(row: Dict[str, Any], floor: int) -> bool:
- try:
- return int(row.get("floor")) == floor
- except (TypeError, ValueError):
- return False
-
-
- def _floor_sort_key(row: Dict[str, Any]) -> int:
- try:
- return int(row.get("floor"))
- except (TypeError, ValueError):
- return 0
-
-
- def _upsert_floor_map_record(floor: int, name: str, metadata: Dict[str, Any]) -> None:
- index = _read_floor_maps_index()
- item = {
- "floor": floor,
- "name": name,
- "mime_type": "image/png",
- "metadata": metadata,
- }
-
- items = [r for r in index["items"] if isinstance(r, dict) and not _same_floor(r, floor)]
- items.append(item)
- items.sort(key=_floor_sort_key)
- index["items"] = items
- _write_floor_maps_index(index)
-
-
- def _public_map_path(name: str) -> str:
- public_path = config_env.RESLEVIS_MAPS_PUBLIC_PATH
- return f"{public_path}/{name}" if public_path else name
-
-
- def _validate_map_target(maps_dir: str, name: str) -> str:
- maps_dir_abs = os.path.abspath(maps_dir)
- target_path = os.path.abspath(os.path.join(maps_dir_abs, name))
- if os.path.commonpath([maps_dir_abs, target_path]) != maps_dir_abs:
- raise HTTPException(status_code=400, detail="Invalid map filename")
- return target_path
-
-
- async def _write_upload_file(upload: UploadFile, target_path: str) -> None:
- maps_dir = os.path.dirname(target_path)
- temp_name = None
- try:
- with tempfile.NamedTemporaryFile("wb", dir=maps_dir, delete=False) as tmp:
- temp_name = tmp.name
- while True:
- chunk = await upload.read(1024 * 1024)
- if not chunk:
- break
- tmp.write(chunk)
- tmp.flush()
- os.fsync(tmp.fileno())
-
- os.replace(temp_name, target_path)
- try:
- os.chmod(target_path, 0o664)
- except OSError:
- pass
- except OSError as exc:
- if temp_name and os.path.exists(temp_name):
- try:
- os.remove(temp_name)
- except OSError:
- pass
- raise HTTPException(status_code=500, detail="Unable to save floor map") from exc
-
- CORE_GET_SYNC = {
- "/reslevis/getGateways": (gateway_repo, _normalize_gateway),
- "/reslevis/getZones": (zone_repo, _normalize_zone),
- }
-
-
- async def _fetch_tracks_for_tracker(
- tracker_id: str,
- params: Optional[dict] = None,
- ) -> List[dict]:
- query_string = urlencode(params or {})
- url = f"{TRACKS_CORE_BASE_URL}/reslevis/getTracks/{tracker_id}"
- if query_string:
- url = f"{url}?{query_string}"
-
- process = await asyncio.create_subprocess_exec(
- "curl",
- "-sS",
- "-X",
- "GET",
- url,
- stdout=asyncio.subprocess.PIPE,
- stderr=asyncio.subprocess.PIPE,
- )
- stdout, stderr = await process.communicate()
-
- if process.returncode != 0:
- detail = (stderr or stdout).decode("utf-8", errors="replace").strip() or "CORE curl request failed"
- raise HTTPException(status_code=502, detail=detail)
-
- try:
- payload = json.loads(stdout.decode("utf-8"))
- except ValueError as exc:
- raise HTTPException(status_code=502, detail="Invalid CORE response") from exc
-
- if not isinstance(payload, list):
- raise HTTPException(status_code=502, detail="Unexpected CORE response type")
-
- return [_normalize_track(row) for row in payload if isinstance(row, dict)]
-
-
- @router.get(
- "/getGateways",
- response_model=List[GatewayItem],
- tags=["Reslevis"],
- dependencies=[Depends(get_current_user)],
- )
- def getGateways():
- return gateway_repo.list()
-
-
- @router.post("/postGateway", tags=["Reslevis"], dependencies=[Depends(get_current_user)])
- def postGateway(item: GatewayItem):
- gateway_repo.add(item)
- return {"message": "OK"}
-
-
- @router.put("/updateGateway", tags=["Reslevis"], dependencies=[Depends(get_current_user)])
- def updateGateway(item: GatewayItem):
- gateway_repo.update(item)
- return {"message": "OK"}
-
-
- @router.delete("/removeGateway/{gateway_id}", tags=["Reslevis"], dependencies=[Depends(get_current_user)])
- def removeGateway(gateway_id: str):
- gateway_repo.remove(gateway_id)
- return {"message": "OK"}
-
-
- @router.get(
- "/getBuildings",
- response_model=List[BuildingItem],
- tags=["Reslevis"],
- dependencies=[Depends(get_current_user)],
- )
- def getBuildings():
- return building_repo.list()
-
-
- @router.post("/postBuilding", tags=["Reslevis"], dependencies=[Depends(get_current_user)])
- def postBuilding(item: BuildingItem):
- building_repo.add(item)
- return {"message": "OK"}
-
-
- @router.put("/updateBuilding", tags=["Reslevis"], dependencies=[Depends(get_current_user)])
- def updateBuilding(item: BuildingItem):
- building_repo.update(item)
- return {"message": "OK"}
-
-
- @router.delete("/removeBuilding/{building_id}", tags=["Reslevis"], dependencies=[Depends(get_current_user)])
- def removeBuilding(building_id: str):
- building_repo.remove(building_id)
- return {"message": "OK"}
-
-
- @router.get(
- "/getFloors",
- response_model=List[FloorItem],
- tags=["Reslevis"],
- dependencies=[Depends(get_current_user)],
- )
-
- def getFloors():
- return floor_repo.list()
-
- @router.post("/postFloor", tags=["Reslevis"], dependencies=[Depends(get_current_user)])
- def postFloor(item: FloorItem):
- floor_repo.add(item)
- return {"message": "OK"}
-
-
- @router.put("/updateFloor", tags=["Reslevis"], dependencies=[Depends(get_current_user)])
- def updateFloor(item: FloorItem):
- floor_repo.update(item)
- return {"message": "OK"}
-
-
- @router.delete("/removeFloor/{floor_id}", tags=["Reslevis"], dependencies=[Depends(get_current_user)])
- def removeFloor(floor_id: str):
- floor_repo.remove(floor_id)
- return {"message": "OK"}
-
-
- @router.get(
- "/getZones",
- response_model=List[ZoneItem],
- tags=["Reslevis"],
- dependencies=[Depends(get_current_user)],
- )
- def getZones():
- return zone_repo.list()
-
-
- @router.post("/postZone", tags=["Reslevis"], dependencies=[Depends(get_current_user)])
- def postZone(item: ZoneItem):
- zone_repo.add(item)
- return {"message": "OK"}
-
-
- @router.put("/updateZone", tags=["Reslevis"], dependencies=[Depends(get_current_user)])
- def updateZone(item: ZoneItem):
- zone_repo.update(item)
- return {"message": "OK"}
-
-
- @router.delete("/removeZone/{zone_id}", tags=["Reslevis"], dependencies=[Depends(get_current_user)])
- def removeZone(zone_id: str):
- zone_repo.remove(zone_id)
- return {"message": "OK"}
-
-
- @router.get(
- "/getZoneAreaDefinitions",
- response_model=List[ZoneAreaDefinitionItem],
- tags=["Reslevis"],
- dependencies=[Depends(get_current_user)],
- )
- def getZoneAreaDefinitions(UUID: str | None = None):
- return zone_area_definition_repo.list(UUID)
-
-
- @router.post("/postZoneAreaDefinition", tags=["Reslevis"], dependencies=[Depends(get_current_user)])
- def postZoneAreaDefinition(item: ZoneAreaDefinitionItem):
- zone_area_definition_repo.add(item)
- return {"message": "OK"}
-
-
- @router.put("/updateZoneAreaDefinition", tags=["Reslevis"], dependencies=[Depends(get_current_user)])
- def updateZoneAreaDefinition(item: ZoneAreaDefinitionItem):
- zone_area_definition_repo.update(item)
- return {"message": "OK"}
-
-
- @router.delete("/removeZoneAreaDefinition/{zone_area_definition_uuid}", tags=["Reslevis"], dependencies=[Depends(get_current_user)])
- def removeZoneAreaDefinition(zone_area_definition_uuid: str):
- zone_area_definition_repo.remove(zone_area_definition_uuid)
- return {"message": "OK"}
-
-
- @router.get(
- "/getTrackers",
- response_model=List[TrackerItem],
- tags=["Reslevis"],
- dependencies=[Depends(get_current_user)],
- )
- async def getTrackers():
- return await get_mode_aware_trackers(
- tracker_repo,
- SETTINGS_CORE_BASE_URL,
- config_env.BLE_AI_INFER_CSV,
- CORE_TIMEOUT,
- )
-
-
- @router.post("/postTracker", tags=["Reslevis"], dependencies=[Depends(get_current_user)])
- def postTracker(item: TrackerItem):
- tracker_repo.add(item)
- return {"message": "OK"}
-
-
- @router.put("/updateTracker", tags=["Reslevis"], dependencies=[Depends(get_current_user)])
- def updateTracker(item: TrackerItem):
- tracker_repo.update(item)
- return {"message": "OK"}
-
-
- @router.delete("/removeTracker/{tracker_id}", tags=["Reslevis"], dependencies=[Depends(get_current_user)])
- def removeTracker(tracker_id: str):
- tracker_repo.remove(tracker_id)
- return {"message": "OK"}
-
-
- @router.get(
- "/getTrackerZones",
- response_model=List[TrackerZoneItem],
- tags=["Reslevis"],
- dependencies=[Depends(get_current_user)],
- )
- def getTrackerZones():
- return tracker_zone_repo.list()
-
-
- @router.post("/postTrackerZone", tags=["Reslevis"], dependencies=[Depends(get_current_user)])
- def postTrackerZone(item: TrackerZoneItem):
- tracker_zone_repo.add(item)
- return {"message": "OK"}
-
-
- @router.put("/updateTrackerZone", tags=["Reslevis"], dependencies=[Depends(get_current_user)])
- def updateTrackerZone(item: TrackerZoneItem):
- tracker_zone_repo.update(item)
- return {"message": "OK"}
-
-
- @router.delete("/removeTrackerZone/{tracker_zone_id}", tags=["Reslevis"], dependencies=[Depends(get_current_user)])
- def removeTrackerZone(tracker_zone_id: str):
- tracker_zone_repo.remove(tracker_zone_id)
- return {"message": "OK"}
-
- @router.get(
- "/getTracks",
- response_model=List[TrackHistoryItem],
- tags=["Reslevis"],
- dependencies=[Depends(get_current_user)],
- )
- async def getTracks(
- tracker_id: str = Query(..., alias="id"),
- limit: Optional[int] = Query(None, ge=1),
- from_: Optional[str] = Query(None, alias="from"),
- to: Optional[str] = Query(None),
- ):
- params = {}
- if limit is not None:
- params["limit"] = limit
- if from_:
- params["from"] = from_
- if to:
- params["to"] = to
-
- return await _fetch_tracks_for_tracker(tracker_id, params)
-
-
- @router.get(
- "/getTracks/{tracker_id}",
- response_model=List[TrackHistoryItem],
- tags=["Reslevis"],
- dependencies=[Depends(get_current_user)],
- )
- async def getTrack(
- tracker_id: str,
- limit: Optional[int] = Query(None, ge=1),
- from_: Optional[str] = Query(None, alias="from"),
- to: Optional[str] = Query(None),
- ):
- params = {}
- if limit is not None:
- params["limit"] = limit
- if from_:
- params["from"] = from_
- if to:
- params["to"] = to
-
- return await _fetch_tracks_for_tracker(tracker_id, params)
-
-
- @router.post("/postTrack", tags=["Reslevis"], dependencies=[Depends(get_current_user)])
- def postTrack(item: TrackItem):
- track_repo.add(item)
- return {"message": "OK"}
-
-
- @router.put("/updateTrack", tags=["Reslevis"], dependencies=[Depends(get_current_user)])
- def updateTrack(item: TrackItem):
- track_repo.update(item)
- return {"message": "OK"}
-
-
- @router.delete("/removeTrack/{track_id}", tags=["Reslevis"], dependencies=[Depends(get_current_user)])
- def removeTrack(track_id: str):
- track_repo.remove(track_id)
- return {"message": "OK"}
-
-
- @router.get(
- "/getAlarms",
- response_model=List[AlarmCoreItem],
- tags=["Reslevis"],
- dependencies=[Depends(get_current_user)],
- )
- async def getAlarms():
- async with httpx.AsyncClient(timeout=CORE_TIMEOUT) as client:
- resp = await client.get(f"{ALERTS_CORE_BASE_URL}/reslevis/alerts")
-
- if resp.status_code >= 400:
- detail = resp.text.strip() or "CORE alerts request failed"
- raise HTTPException(status_code=resp.status_code, detail=detail)
-
- try:
- payload = resp.json()
- except ValueError as exc:
- raise HTTPException(status_code=502, detail="Invalid CORE response") from exc
-
- if not isinstance(payload, list):
- raise HTTPException(status_code=502, detail="Unexpected CORE response type")
-
- return payload
-
-
- @router.put(
- "/updateAlarm",
- tags=["Reslevis"],
- dependencies=[Depends(get_current_user)],
- )
- async def updateAlarm(item: AlarmStatusUpdateItem):
- async with httpx.AsyncClient(timeout=CORE_TIMEOUT) as client:
- resp = await client.patch(
- f"{ALERTS_CORE_BASE_URL}/reslevis/alerts/{item.id}",
- json={"status": item.status},
- )
-
- if resp.status_code >= 400:
- detail = resp.text.strip() or "CORE alert update failed"
- raise HTTPException(status_code=resp.status_code, detail=detail)
-
- if not resp.content:
- return {"message": "OK"}
-
- try:
- return resp.json()
- except ValueError:
- return {"message": "OK"}
-
-
- @router.get(
- "/getOperators",
- response_model=List[OperatorItem],
- tags=["Reslevis"],
- dependencies=[Depends(get_current_user)],
- )
- def getOperators():
- return operator_repo.list()
-
-
- @router.post("/postOperator", tags=["Reslevis"], dependencies=[Depends(get_current_user)])
- def postOperator(item: OperatorItem):
- operator_repo.add(item)
- return {"message": "OK"}
-
-
- @router.put("/updateOperator", tags=["Reslevis"], dependencies=[Depends(get_current_user)])
- def updateOperator(item: OperatorItem):
- operator_repo.update(item)
- return {"message": "OK"}
-
-
- @router.delete("/removeOperator/{operator_id}", tags=["Reslevis"], dependencies=[Depends(get_current_user)])
- def removeOperator(operator_id: str):
- operator_repo.remove(operator_id)
- return {"message": "OK"}
-
-
- @router.get(
- "/getSubjects",
- response_model=List[SubjectItem],
- tags=["Reslevis"],
- dependencies=[Depends(get_current_user)],
- )
- def getSubjects():
- return subject_repo.list()
-
-
- @router.post("/postSubject", tags=["Reslevis"], dependencies=[Depends(get_current_user)])
- def postSubject(item: SubjectItem):
- subject_repo.add(item)
- return {"message": "OK"}
-
-
- @router.put("/updateSubject", tags=["Reslevis"], dependencies=[Depends(get_current_user)])
- def updateSubject(item: SubjectItem):
- subject_repo.update(item)
- return {"message": "OK"}
-
-
- @router.delete("/removeSubject/{subject_id}", tags=["Reslevis"], dependencies=[Depends(get_current_user)])
- def removeSubject(subject_id: str):
- subject_repo.remove(subject_id)
- return {"message": "OK"}
-
-
- @router.get(
- "/getSettings",
- response_model=List[SettingItem],
- tags=["Reslevis"],
- dependencies=[Depends(get_current_user)],
- )
- def getSettings():
- return setting_repo.list()
-
-
- @router.post("/postSetting", tags=["Reslevis"], dependencies=[Depends(get_current_user)])
- def postSetting(item: SettingItem):
- setting_repo.add(item)
- return {"message": "OK"}
-
-
- @router.put("/updateSetting", tags=["Reslevis"], dependencies=[Depends(get_current_user)])
- def updateSetting(item: SettingItem):
- setting_repo.update(item)
- return {"message": "OK"}
-
-
- @router.delete("/removeSetting/{setting_id}", tags=["Reslevis"], dependencies=[Depends(get_current_user)])
- def removeSetting(setting_id: str):
- setting_repo.remove(setting_id)
- return {"message": "OK"}
-
-
- @router.get(
- "/getGuiConfigs",
- response_model=List[GuiConfigItem],
- tags=["Reslevis"],
- dependencies=[Depends(get_current_user)],
- )
- def getGuiConfigs():
- return gui_config_repo.list()
-
-
- @router.get(
- "/getUserPreferences",
- response_model=UserPreferencesItem,
- tags=["Reslevis"],
- dependencies=[Depends(get_current_user)],
- )
- def getUserPreferences(current_user: dict = Depends(get_current_user)):
- uid = _uid_from_claims(current_user)
- return user_preferences_repo.get(uid)
-
-
- @router.put(
- "/updateUserPreferences",
- response_model=UserPreferencesItem,
- tags=["Reslevis"],
- dependencies=[Depends(get_current_user)],
- )
- def updateUserPreferences(
- item: UserPreferencesUpdateItem,
- current_user: dict = Depends(get_current_user),
- ):
- uid = _uid_from_claims(current_user)
- return user_preferences_repo.update(uid, item)
-
-
- @router.post(
- "/uploadFloorMap",
- response_model=FloorMapUploadResponseItem,
- tags=["Reslevis"],
- dependencies=[Depends(get_current_user)],
- )
- async def uploadFloorMap(
- floor: int = Form(...),
- file: UploadFile = File(...),
- pixel_ratio: float = Form(1),
- calibrated: bool = Form(False),
- origin_x: int = Form(0),
- origin_y: int = Form(0),
- grid_size: int = Form(50),
- ):
- if pixel_ratio <= 0:
- raise HTTPException(status_code=400, detail="pixel_ratio must be greater than 0")
- if grid_size <= 0:
- raise HTTPException(status_code=400, detail="grid_size must be greater than 0")
-
- content_type = (file.content_type or "").split(";")[0].strip().lower()
- if content_type and content_type != "image/png":
- raise HTTPException(status_code=400, detail="Only PNG files are allowed")
-
- signature = await file.read(len(PNG_SIGNATURE))
- if signature != PNG_SIGNATURE:
- raise HTTPException(status_code=400, detail="Only PNG files are allowed")
- await file.seek(0)
-
- maps_dir = config_env.RESLEVIS_MAPS_DIR
- os.makedirs(maps_dir, exist_ok=True)
-
- name = f"floor_{floor}.png"
- target_path = _validate_map_target(maps_dir, name)
- await _write_upload_file(file, target_path)
-
- metadata = {
- "pixel_ratio": pixel_ratio,
- "calibrated": calibrated,
- "origin": [origin_x, origin_y],
- "grid_size": grid_size,
- }
- _upsert_floor_map_record(floor, name, metadata)
-
- return {
- "floor": floor,
- "name": name,
- "path": _public_map_path(name),
- "mime_type": "image/png",
- "metadata": metadata,
- }
-
-
- @router.post("/postGuiConfig", tags=["Reslevis"], dependencies=[Depends(get_current_user)])
- def postGuiConfig(item: GuiConfigItem):
- gui_config_repo.add(item)
- return {"message": "OK"}
-
-
- @router.put("/updateGuiConfig", tags=["Reslevis"], dependencies=[Depends(get_current_user)])
- def updateGuiConfig(item: GuiConfigItem):
- gui_config_repo.update(item)
- return {"message": "OK"}
-
-
- @router.delete("/removeGuiConfig/{gui_config_id}", tags=["Reslevis"], dependencies=[Depends(get_current_user)])
- def removeGuiConfig(gui_config_id: str):
- gui_config_repo.remove(gui_config_id)
- return {"message": "OK"}
-
-
- @router.get(
- "/getCoreSettings",
- response_model=List[CoreSettingsItem],
- tags=["Reslevis"],
- dependencies=[Depends(get_current_user)],
- )
- async def getCoreSettings():
- async with httpx.AsyncClient(timeout=CORE_TIMEOUT) as client:
- resp = await client.get(f"{SETTINGS_CORE_BASE_URL}/reslevis/settings")
-
- if resp.status_code >= 400:
- detail = resp.text.strip() or "CORE settings request failed"
- raise HTTPException(status_code=resp.status_code, detail=detail)
-
- try:
- payload = resp.json()
- except ValueError as exc:
- raise HTTPException(status_code=502, detail="Invalid CORE response") from exc
-
- if not isinstance(payload, list):
- raise HTTPException(status_code=502, detail="Unexpected CORE response type")
-
- return payload
-
-
- @router.put(
- "/updateCoreSettings",
- tags=["Reslevis"],
- dependencies=[Depends(get_current_user)],
- )
- async def updateCoreSettings(item: CoreSettingsUpdateItem):
- async with httpx.AsyncClient(timeout=CORE_TIMEOUT) as client:
- resp = await client.patch(
- f"{SETTINGS_CORE_BASE_URL}/reslevis/settings",
- json=item.model_dump(exclude_none=True),
- )
-
- if resp.status_code >= 400:
- detail = resp.text.strip() or "CORE settings update failed"
- raise HTTPException(status_code=resp.status_code, detail=detail)
-
- if not resp.content:
- return {"message": "OK"}
-
- try:
- return resp.json()
- except ValueError:
- return {"message": "OK"}
|