import asyncio import json import os import tempfile from urllib.parse import urlencode from uuid import UUID 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 _uuid_or_none(value): if value in ("", None, 0, "0"): return None try: return str(UUID(str(value))) except (TypeError, ValueError, AttributeError): return None def _operator_uuid_by_name(value): name = str(value).strip() if not name: return None try: rows = operator_repo.list() except Exception: return None target = name.lower() for row in rows: if not isinstance(row, dict): continue if str(row.get("name") or "").strip().lower() == target: return _uuid_or_none(row.get("id")) return None def _normalize_alarm_core(row: dict) -> dict: row = dict(row) operator_raw = row.get("operator") operator_uuid = _uuid_or_none(operator_raw) if operator_uuid is None and operator_raw not in ("", None, 0, "0"): operator_uuid = _operator_uuid_by_name(operator_raw) if operator_uuid is None and not row.get("operatorName"): row["operatorName"] = str(operator_raw) row["operator"] = operator_uuid return row 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 [_normalize_alarm_core(row) for row in payload if isinstance(row, dict)] @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"}