You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

712 regels
21 KiB

  1. import asyncio
  2. import json
  3. from urllib.parse import urlencode
  4. from fastapi import APIRouter, Depends, HTTPException, Query, Request
  5. import httpx
  6. import config_env
  7. from typing import List, Optional
  8. from schemas.reslevis import (
  9. BuildingItem,
  10. FloorItem,
  11. ZoneItem,
  12. ZoneAreaDefinitionItem,
  13. GatewayItem,
  14. TrackerItem,
  15. OperatorItem,
  16. SubjectItem,
  17. AlarmItem,
  18. AlarmCoreItem,
  19. AlarmStatusUpdateItem,
  20. TrackItem,
  21. TrackHistoryItem,
  22. TrackerZoneItem,
  23. SettingItem,
  24. GuiConfigItem,
  25. UserPreferencesItem,
  26. UserPreferencesUpdateItem,
  27. CoreSettingsItem,
  28. CoreSettingsUpdateItem,
  29. )
  30. from logica_reslevis.gateway import GatewayJsonRepository
  31. from logica_reslevis.building import BuildingJsonRepository
  32. from logica_reslevis.floor import FloorJsonRepository
  33. from logica_reslevis.zone import ZoneJsonRepository
  34. from logica_reslevis.zone_area_definition import ZoneAreaDefinitionJsonRepository
  35. from logica_reslevis.tracker import TrackerJsonRepository
  36. from logica_reslevis.operator import OperatorJsonRepository
  37. from logica_reslevis.setting import SettingJsonRepository
  38. from logica_reslevis.gui_config import GuiConfigJsonRepository
  39. from logica_reslevis.user_preferences import UserPreferencesJsonRepository
  40. from logica_reslevis.subject import SubjectJsonRepository
  41. from logica_reslevis.alarm import AlarmJsonRepository
  42. from logica_reslevis.track import TrackJsonRepository
  43. from logica_reslevis.tracker_zone import TrackerZoneJsonRepository
  44. from logica_reslevis.tracker_mode import get_mode_aware_trackers
  45. from security import get_current_user
  46. #CORE SYNC
  47. CORE_BASE_URL = config_env.CORE_API_URL.rstrip("/")
  48. ALERTS_CORE_BASE_URL = "http://localhost:1902"
  49. TRACKS_CORE_BASE_URL = "http://localhost:1902"
  50. SETTINGS_CORE_BASE_URL = "http://127.0.0.1:1902"
  51. CORE_TIMEOUT = 2.0 # secondi
  52. async def sync_core_get(request: Request) -> None:
  53. if request.method != "GET":
  54. return
  55. sync = CORE_GET_SYNC.get(request.url.path)
  56. if sync is None:
  57. return
  58. repo, normalizer = sync
  59. try:
  60. async with httpx.AsyncClient(timeout=CORE_TIMEOUT) as client:
  61. resp = await client.get(
  62. f"{CORE_BASE_URL}{request.url.path}",
  63. params=request.query_params,
  64. )
  65. if 200 <= resp.status_code < 300:
  66. data = resp.json()
  67. if isinstance(data, list):
  68. if normalizer:
  69. data = [normalizer(r) for r in data if isinstance(r, dict)]
  70. repo._write_all(data) # aggiorna i file locali
  71. except (httpx.RequestError, ValueError):
  72. # CORE giù o risposta non valida -> uso il file locale
  73. pass
  74. router = APIRouter(dependencies=[Depends(sync_core_get)])
  75. gateway_repo = GatewayJsonRepository()
  76. building_repo = BuildingJsonRepository()
  77. floor_repo = FloorJsonRepository()
  78. zone_repo = ZoneJsonRepository()
  79. zone_area_definition_repo = ZoneAreaDefinitionJsonRepository()
  80. tracker_repo = TrackerJsonRepository()
  81. operator_repo = OperatorJsonRepository()
  82. subject_repo = SubjectJsonRepository()
  83. alarm_repo = AlarmJsonRepository()
  84. track_repo = TrackJsonRepository()
  85. tracker_zone_repo = TrackerZoneJsonRepository()
  86. setting_repo = SettingJsonRepository()
  87. gui_config_repo = GuiConfigJsonRepository()
  88. user_preferences_repo = UserPreferencesJsonRepository()
  89. def _none_if_empty(v):
  90. return None if v in ("", None, 0, "0") else v
  91. def _str_or_none(v):
  92. if v in ("", None):
  93. return None
  94. if isinstance(v, (int, float, bool)):
  95. return str(v)
  96. return v
  97. def _uuid_list(values):
  98. if values in ("", None):
  99. return []
  100. if isinstance(values, str):
  101. values = [v for v in values.split(",") if v]
  102. if isinstance(values, (list, tuple, set)):
  103. cleaned = []
  104. for v in values:
  105. if isinstance(v, dict):
  106. v = v.get("id") or v.get("uuid")
  107. if v in ("", None, 0, "0"):
  108. continue
  109. cleaned.append(v)
  110. return cleaned
  111. return [values] if values not in ("", None, 0, "0") else []
  112. def _normalize_gateway(row: dict) -> dict:
  113. row = dict(row)
  114. row["floor"] = _none_if_empty(row.get("floor"))
  115. row["building"] = _none_if_empty(row.get("building"))
  116. return row
  117. def _normalize_track(row: dict) -> dict:
  118. row = dict(row)
  119. row["ID"] = row.get("ID")
  120. row["gateway"] = _none_if_empty(row.get("gateway"))
  121. row["tracker"] = _none_if_empty(row.get("tracker"))
  122. row["subject"] = _none_if_empty(row.get("subject"))
  123. row["floor"] = _none_if_empty(row.get("floor"))
  124. row["building"] = _none_if_empty(row.get("building"))
  125. row["timestamp"] = _str_or_none(row.get("timestamp"))
  126. row["type"] = _str_or_none(row.get("type"))
  127. row["status"] = _str_or_none(row.get("status"))
  128. row["gatewayMac"] = _str_or_none(row.get("gatewayMac"))
  129. row["trackerMac"] = _str_or_none(row.get("trackerMac"))
  130. row["subjectName"] = _str_or_none(row.get("subjectName"))
  131. row["x"] = None if row.get("x") in ("", None) else row.get("x")
  132. row["y"] = None if row.get("y") in ("", None) else row.get("y")
  133. row["z"] = None if row.get("z") in ("", None) else row.get("z")
  134. # signal resta float o None
  135. row["signal"] = None if row.get("signal") in ("", None) else row.get("signal")
  136. return row
  137. def _normalize_zone(row: dict) -> dict:
  138. row = dict(row)
  139. row["floor"] = _none_if_empty(row.get("floor"))
  140. row["building"] = _none_if_empty(row.get("building"))
  141. row["groups"] = _uuid_list(row.get("groups"))
  142. return row
  143. def _uid_from_claims(claims: dict) -> str:
  144. uid = (
  145. claims.get("preferred_username")
  146. or claims.get("username")
  147. or claims.get("email")
  148. or claims.get("sub")
  149. )
  150. if not uid:
  151. raise HTTPException(status_code=401, detail="User identity not found in token")
  152. return str(uid)
  153. CORE_GET_SYNC = {
  154. "/reslevis/getGateways": (gateway_repo, _normalize_gateway),
  155. "/reslevis/getZones": (zone_repo, _normalize_zone),
  156. }
  157. async def _fetch_tracks_for_tracker(
  158. tracker_id: str,
  159. params: Optional[dict] = None,
  160. ) -> List[dict]:
  161. query_string = urlencode(params or {})
  162. url = f"{TRACKS_CORE_BASE_URL}/reslevis/getTracks/{tracker_id}"
  163. if query_string:
  164. url = f"{url}?{query_string}"
  165. process = await asyncio.create_subprocess_exec(
  166. "curl",
  167. "-sS",
  168. "-X",
  169. "GET",
  170. url,
  171. stdout=asyncio.subprocess.PIPE,
  172. stderr=asyncio.subprocess.PIPE,
  173. )
  174. stdout, stderr = await process.communicate()
  175. if process.returncode != 0:
  176. detail = (stderr or stdout).decode("utf-8", errors="replace").strip() or "CORE curl request failed"
  177. raise HTTPException(status_code=502, detail=detail)
  178. try:
  179. payload = json.loads(stdout.decode("utf-8"))
  180. except ValueError as exc:
  181. raise HTTPException(status_code=502, detail="Invalid CORE response") from exc
  182. if not isinstance(payload, list):
  183. raise HTTPException(status_code=502, detail="Unexpected CORE response type")
  184. return [_normalize_track(row) for row in payload if isinstance(row, dict)]
  185. @router.get(
  186. "/getGateways",
  187. response_model=List[GatewayItem],
  188. tags=["Reslevis"],
  189. dependencies=[Depends(get_current_user)],
  190. )
  191. def getGateways():
  192. return gateway_repo.list()
  193. @router.post("/postGateway", tags=["Reslevis"], dependencies=[Depends(get_current_user)])
  194. def postGateway(item: GatewayItem):
  195. gateway_repo.add(item)
  196. return {"message": "OK"}
  197. @router.put("/updateGateway", tags=["Reslevis"], dependencies=[Depends(get_current_user)])
  198. def updateGateway(item: GatewayItem):
  199. gateway_repo.update(item)
  200. return {"message": "OK"}
  201. @router.delete("/removeGateway/{gateway_id}", tags=["Reslevis"], dependencies=[Depends(get_current_user)])
  202. def removeGateway(gateway_id: str):
  203. gateway_repo.remove(gateway_id)
  204. return {"message": "OK"}
  205. @router.get(
  206. "/getBuildings",
  207. response_model=List[BuildingItem],
  208. tags=["Reslevis"],
  209. dependencies=[Depends(get_current_user)],
  210. )
  211. def getBuildings():
  212. return building_repo.list()
  213. @router.post("/postBuilding", tags=["Reslevis"], dependencies=[Depends(get_current_user)])
  214. def postBuilding(item: BuildingItem):
  215. building_repo.add(item)
  216. return {"message": "OK"}
  217. @router.put("/updateBuilding", tags=["Reslevis"], dependencies=[Depends(get_current_user)])
  218. def updateBuilding(item: BuildingItem):
  219. building_repo.update(item)
  220. return {"message": "OK"}
  221. @router.delete("/removeBuilding/{building_id}", tags=["Reslevis"], dependencies=[Depends(get_current_user)])
  222. def removeBuilding(building_id: str):
  223. building_repo.remove(building_id)
  224. return {"message": "OK"}
  225. @router.get(
  226. "/getFloors",
  227. response_model=List[FloorItem],
  228. tags=["Reslevis"],
  229. dependencies=[Depends(get_current_user)],
  230. )
  231. def getFloors():
  232. return floor_repo.list()
  233. @router.post("/postFloor", tags=["Reslevis"], dependencies=[Depends(get_current_user)])
  234. def postFloor(item: FloorItem):
  235. floor_repo.add(item)
  236. return {"message": "OK"}
  237. @router.put("/updateFloor", tags=["Reslevis"], dependencies=[Depends(get_current_user)])
  238. def updateFloor(item: FloorItem):
  239. floor_repo.update(item)
  240. return {"message": "OK"}
  241. @router.delete("/removeFloor/{floor_id}", tags=["Reslevis"], dependencies=[Depends(get_current_user)])
  242. def removeFloor(floor_id: str):
  243. floor_repo.remove(floor_id)
  244. return {"message": "OK"}
  245. @router.get(
  246. "/getZones",
  247. response_model=List[ZoneItem],
  248. tags=["Reslevis"],
  249. dependencies=[Depends(get_current_user)],
  250. )
  251. def getZones():
  252. return zone_repo.list()
  253. @router.post("/postZone", tags=["Reslevis"], dependencies=[Depends(get_current_user)])
  254. def postZone(item: ZoneItem):
  255. zone_repo.add(item)
  256. return {"message": "OK"}
  257. @router.put("/updateZone", tags=["Reslevis"], dependencies=[Depends(get_current_user)])
  258. def updateZone(item: ZoneItem):
  259. zone_repo.update(item)
  260. return {"message": "OK"}
  261. @router.delete("/removeZone/{zone_id}", tags=["Reslevis"], dependencies=[Depends(get_current_user)])
  262. def removeZone(zone_id: str):
  263. zone_repo.remove(zone_id)
  264. return {"message": "OK"}
  265. @router.get(
  266. "/getZoneAreaDefinitions",
  267. response_model=List[ZoneAreaDefinitionItem],
  268. tags=["Reslevis"],
  269. dependencies=[Depends(get_current_user)],
  270. )
  271. def getZoneAreaDefinitions(UUID: str | None = None):
  272. return zone_area_definition_repo.list(UUID)
  273. @router.post("/postZoneAreaDefinition", tags=["Reslevis"], dependencies=[Depends(get_current_user)])
  274. def postZoneAreaDefinition(item: ZoneAreaDefinitionItem):
  275. zone_area_definition_repo.add(item)
  276. return {"message": "OK"}
  277. @router.put("/updateZoneAreaDefinition", tags=["Reslevis"], dependencies=[Depends(get_current_user)])
  278. def updateZoneAreaDefinition(item: ZoneAreaDefinitionItem):
  279. zone_area_definition_repo.update(item)
  280. return {"message": "OK"}
  281. @router.delete("/removeZoneAreaDefinition/{zone_area_definition_uuid}", tags=["Reslevis"], dependencies=[Depends(get_current_user)])
  282. def removeZoneAreaDefinition(zone_area_definition_uuid: str):
  283. zone_area_definition_repo.remove(zone_area_definition_uuid)
  284. return {"message": "OK"}
  285. @router.get(
  286. "/getTrackers",
  287. response_model=List[TrackerItem],
  288. tags=["Reslevis"],
  289. dependencies=[Depends(get_current_user)],
  290. )
  291. async def getTrackers():
  292. return await get_mode_aware_trackers(
  293. tracker_repo,
  294. SETTINGS_CORE_BASE_URL,
  295. config_env.BLE_AI_INFER_CSV,
  296. CORE_TIMEOUT,
  297. )
  298. @router.post("/postTracker", tags=["Reslevis"], dependencies=[Depends(get_current_user)])
  299. def postTracker(item: TrackerItem):
  300. tracker_repo.add(item)
  301. return {"message": "OK"}
  302. @router.put("/updateTracker", tags=["Reslevis"], dependencies=[Depends(get_current_user)])
  303. def updateTracker(item: TrackerItem):
  304. tracker_repo.update(item)
  305. return {"message": "OK"}
  306. @router.delete("/removeTracker/{tracker_id}", tags=["Reslevis"], dependencies=[Depends(get_current_user)])
  307. def removeTracker(tracker_id: str):
  308. tracker_repo.remove(tracker_id)
  309. return {"message": "OK"}
  310. @router.get(
  311. "/getTrackerZones",
  312. response_model=List[TrackerZoneItem],
  313. tags=["Reslevis"],
  314. dependencies=[Depends(get_current_user)],
  315. )
  316. def getTrackerZones():
  317. return tracker_zone_repo.list()
  318. @router.post("/postTrackerZone", tags=["Reslevis"], dependencies=[Depends(get_current_user)])
  319. def postTrackerZone(item: TrackerZoneItem):
  320. tracker_zone_repo.add(item)
  321. return {"message": "OK"}
  322. @router.put("/updateTrackerZone", tags=["Reslevis"], dependencies=[Depends(get_current_user)])
  323. def updateTrackerZone(item: TrackerZoneItem):
  324. tracker_zone_repo.update(item)
  325. return {"message": "OK"}
  326. @router.delete("/removeTrackerZone/{tracker_zone_id}", tags=["Reslevis"], dependencies=[Depends(get_current_user)])
  327. def removeTrackerZone(tracker_zone_id: str):
  328. tracker_zone_repo.remove(tracker_zone_id)
  329. return {"message": "OK"}
  330. @router.get(
  331. "/getTracks",
  332. response_model=List[TrackHistoryItem],
  333. tags=["Reslevis"],
  334. dependencies=[Depends(get_current_user)],
  335. )
  336. async def getTracks(
  337. tracker_id: str = Query(..., alias="id"),
  338. limit: Optional[int] = Query(None, ge=1),
  339. from_: Optional[str] = Query(None, alias="from"),
  340. to: Optional[str] = Query(None),
  341. ):
  342. params = {}
  343. if limit is not None:
  344. params["limit"] = limit
  345. if from_:
  346. params["from"] = from_
  347. if to:
  348. params["to"] = to
  349. return await _fetch_tracks_for_tracker(tracker_id, params)
  350. @router.get(
  351. "/getTracks/{tracker_id}",
  352. response_model=List[TrackHistoryItem],
  353. tags=["Reslevis"],
  354. dependencies=[Depends(get_current_user)],
  355. )
  356. async def getTrack(
  357. tracker_id: str,
  358. limit: Optional[int] = Query(None, ge=1),
  359. from_: Optional[str] = Query(None, alias="from"),
  360. to: Optional[str] = Query(None),
  361. ):
  362. params = {}
  363. if limit is not None:
  364. params["limit"] = limit
  365. if from_:
  366. params["from"] = from_
  367. if to:
  368. params["to"] = to
  369. return await _fetch_tracks_for_tracker(tracker_id, params)
  370. @router.post("/postTrack", tags=["Reslevis"], dependencies=[Depends(get_current_user)])
  371. def postTrack(item: TrackItem):
  372. track_repo.add(item)
  373. return {"message": "OK"}
  374. @router.put("/updateTrack", tags=["Reslevis"], dependencies=[Depends(get_current_user)])
  375. def updateTrack(item: TrackItem):
  376. track_repo.update(item)
  377. return {"message": "OK"}
  378. @router.delete("/removeTrack/{track_id}", tags=["Reslevis"], dependencies=[Depends(get_current_user)])
  379. def removeTrack(track_id: str):
  380. track_repo.remove(track_id)
  381. return {"message": "OK"}
  382. @router.get(
  383. "/getAlarms",
  384. response_model=List[AlarmCoreItem],
  385. tags=["Reslevis"],
  386. dependencies=[Depends(get_current_user)],
  387. )
  388. async def getAlarms():
  389. async with httpx.AsyncClient(timeout=CORE_TIMEOUT) as client:
  390. resp = await client.get(f"{ALERTS_CORE_BASE_URL}/reslevis/alerts")
  391. if resp.status_code >= 400:
  392. detail = resp.text.strip() or "CORE alerts request failed"
  393. raise HTTPException(status_code=resp.status_code, detail=detail)
  394. try:
  395. payload = resp.json()
  396. except ValueError as exc:
  397. raise HTTPException(status_code=502, detail="Invalid CORE response") from exc
  398. if not isinstance(payload, list):
  399. raise HTTPException(status_code=502, detail="Unexpected CORE response type")
  400. return payload
  401. @router.put(
  402. "/updateAlarm",
  403. tags=["Reslevis"],
  404. dependencies=[Depends(get_current_user)],
  405. )
  406. async def updateAlarm(item: AlarmStatusUpdateItem):
  407. async with httpx.AsyncClient(timeout=CORE_TIMEOUT) as client:
  408. resp = await client.patch(
  409. f"{ALERTS_CORE_BASE_URL}/reslevis/alerts/{item.id}",
  410. json={"status": item.status},
  411. )
  412. if resp.status_code >= 400:
  413. detail = resp.text.strip() or "CORE alert update failed"
  414. raise HTTPException(status_code=resp.status_code, detail=detail)
  415. if not resp.content:
  416. return {"message": "OK"}
  417. try:
  418. return resp.json()
  419. except ValueError:
  420. return {"message": "OK"}
  421. @router.get(
  422. "/getOperators",
  423. response_model=List[OperatorItem],
  424. tags=["Reslevis"],
  425. dependencies=[Depends(get_current_user)],
  426. )
  427. def getOperators():
  428. return operator_repo.list()
  429. @router.post("/postOperator", tags=["Reslevis"], dependencies=[Depends(get_current_user)])
  430. def postOperator(item: OperatorItem):
  431. operator_repo.add(item)
  432. return {"message": "OK"}
  433. @router.put("/updateOperator", tags=["Reslevis"], dependencies=[Depends(get_current_user)])
  434. def updateOperator(item: OperatorItem):
  435. operator_repo.update(item)
  436. return {"message": "OK"}
  437. @router.delete("/removeOperator/{operator_id}", tags=["Reslevis"], dependencies=[Depends(get_current_user)])
  438. def removeOperator(operator_id: str):
  439. operator_repo.remove(operator_id)
  440. return {"message": "OK"}
  441. @router.get(
  442. "/getSubjects",
  443. response_model=List[SubjectItem],
  444. tags=["Reslevis"],
  445. dependencies=[Depends(get_current_user)],
  446. )
  447. def getSubjects():
  448. return subject_repo.list()
  449. @router.post("/postSubject", tags=["Reslevis"], dependencies=[Depends(get_current_user)])
  450. def postSubject(item: SubjectItem):
  451. subject_repo.add(item)
  452. return {"message": "OK"}
  453. @router.put("/updateSubject", tags=["Reslevis"], dependencies=[Depends(get_current_user)])
  454. def updateSubject(item: SubjectItem):
  455. subject_repo.update(item)
  456. return {"message": "OK"}
  457. @router.delete("/removeSubject/{subject_id}", tags=["Reslevis"], dependencies=[Depends(get_current_user)])
  458. def removeSubject(subject_id: str):
  459. subject_repo.remove(subject_id)
  460. return {"message": "OK"}
  461. @router.get(
  462. "/getSettings",
  463. response_model=List[SettingItem],
  464. tags=["Reslevis"],
  465. dependencies=[Depends(get_current_user)],
  466. )
  467. def getSettings():
  468. return setting_repo.list()
  469. @router.post("/postSetting", tags=["Reslevis"], dependencies=[Depends(get_current_user)])
  470. def postSetting(item: SettingItem):
  471. setting_repo.add(item)
  472. return {"message": "OK"}
  473. @router.put("/updateSetting", tags=["Reslevis"], dependencies=[Depends(get_current_user)])
  474. def updateSetting(item: SettingItem):
  475. setting_repo.update(item)
  476. return {"message": "OK"}
  477. @router.delete("/removeSetting/{setting_id}", tags=["Reslevis"], dependencies=[Depends(get_current_user)])
  478. def removeSetting(setting_id: str):
  479. setting_repo.remove(setting_id)
  480. return {"message": "OK"}
  481. @router.get(
  482. "/getGuiConfigs",
  483. response_model=List[GuiConfigItem],
  484. tags=["Reslevis"],
  485. dependencies=[Depends(get_current_user)],
  486. )
  487. def getGuiConfigs():
  488. return gui_config_repo.list()
  489. @router.get(
  490. "/getUserPreferences",
  491. response_model=UserPreferencesItem,
  492. tags=["Reslevis"],
  493. )
  494. def getUserPreferences(current_user: dict = Depends(get_current_user)):
  495. uid = _uid_from_claims(current_user)
  496. return user_preferences_repo.get(uid)
  497. @router.put(
  498. "/updateUserPreferences",
  499. response_model=UserPreferencesItem,
  500. tags=["Reslevis"],
  501. )
  502. def updateUserPreferences(
  503. item: UserPreferencesUpdateItem,
  504. current_user: dict = Depends(get_current_user),
  505. ):
  506. uid = _uid_from_claims(current_user)
  507. return user_preferences_repo.update(uid, item)
  508. @router.post("/postGuiConfig", tags=["Reslevis"], dependencies=[Depends(get_current_user)])
  509. def postGuiConfig(item: GuiConfigItem):
  510. gui_config_repo.add(item)
  511. return {"message": "OK"}
  512. @router.put("/updateGuiConfig", tags=["Reslevis"], dependencies=[Depends(get_current_user)])
  513. def updateGuiConfig(item: GuiConfigItem):
  514. gui_config_repo.update(item)
  515. return {"message": "OK"}
  516. @router.delete("/removeGuiConfig/{gui_config_id}", tags=["Reslevis"], dependencies=[Depends(get_current_user)])
  517. def removeGuiConfig(gui_config_id: str):
  518. gui_config_repo.remove(gui_config_id)
  519. return {"message": "OK"}
  520. @router.get(
  521. "/getCoreSettings",
  522. response_model=List[CoreSettingsItem],
  523. tags=["Reslevis"],
  524. dependencies=[Depends(get_current_user)],
  525. )
  526. async def getCoreSettings():
  527. async with httpx.AsyncClient(timeout=CORE_TIMEOUT) as client:
  528. resp = await client.get(f"{SETTINGS_CORE_BASE_URL}/reslevis/settings")
  529. if resp.status_code >= 400:
  530. detail = resp.text.strip() or "CORE settings request failed"
  531. raise HTTPException(status_code=resp.status_code, detail=detail)
  532. try:
  533. payload = resp.json()
  534. except ValueError as exc:
  535. raise HTTPException(status_code=502, detail="Invalid CORE response") from exc
  536. if not isinstance(payload, list):
  537. raise HTTPException(status_code=502, detail="Unexpected CORE response type")
  538. return payload
  539. @router.put(
  540. "/updateCoreSettings",
  541. tags=["Reslevis"],
  542. dependencies=[Depends(get_current_user)],
  543. )
  544. async def updateCoreSettings(item: CoreSettingsUpdateItem):
  545. async with httpx.AsyncClient(timeout=CORE_TIMEOUT) as client:
  546. resp = await client.patch(
  547. f"{SETTINGS_CORE_BASE_URL}/reslevis/settings",
  548. json=item.model_dump(exclude_none=True),
  549. )
  550. if resp.status_code >= 400:
  551. detail = resp.text.strip() or "CORE settings update failed"
  552. raise HTTPException(status_code=resp.status_code, detail=detail)
  553. if not resp.content:
  554. return {"message": "OK"}
  555. try:
  556. return resp.json()
  557. except ValueError:
  558. return {"message": "OK"}