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"}