diff --git a/api_client.py b/api_client.py index 16c29fd..2a3bfb3 100644 --- a/api_client.py +++ b/api_client.py @@ -19,10 +19,14 @@ def get_http_client() -> httpx.AsyncClient: ) return _http_client +def get_cloud_base_url() -> str: + url = getattr(settings, "cloud_api_url", None) or os.getenv("CLOUD_API_URL") or "https://services-api.a2zhealth.in/v1/contec-monitor" + return url.rstrip("/") + async def forward_vitals_to_api(vitals: NormalizedVitals) -> bool: """ Attempts to POST the normalized vitals to the configured target API - and to the Render cloud database concurrently. + and to the Cloud server concurrently. Returns True if at least one forward succeeds. """ headers = {"Content-Type": "application/json"} @@ -59,16 +63,17 @@ async def forward_vitals_to_api(vitals: NormalizedVitals) -> bool: logger.warning(f"Error forwarding to primary API: {type(e).__name__} ({err_msg})") return False - # 2. Forward to Render Cloud Server - async def _forward_render() -> bool: - render_url = "https://contectfiveparamonitor.onrender.com/api/push-vitals" - is_on_render = os.getenv("RENDER") is not None or os.getenv("PORT") == "10000" or "render.local" in os.getenv("HOSTNAME", "") - if is_on_render: + # 2. Forward to Cloud Server + async def _forward_cloud() -> bool: + cloud_base = get_cloud_base_url() + cloud_url = f"{cloud_base}/api/push-vitals" + is_on_cloud = os.getenv("RENDER") is not None or os.getenv("PORT") == "10000" or "render.local" in os.getenv("HOSTNAME", "") + if is_on_cloud and ("onrender.com" in cloud_base): return True try: - url_with_key = render_url + url_with_key = cloud_url if settings.api_token: - url_with_key = f"{render_url}?api_key={settings.api_token}" + url_with_key = f"{cloud_url}?api_key={settings.api_token}" response = await client.post( url_with_key, @@ -76,28 +81,29 @@ async def forward_vitals_to_api(vitals: NormalizedVitals) -> bool: headers=headers ) response.raise_for_status() - logger.info(f"Successfully forwarded vitals to Render Cloud for device {vitals.device_id}") + logger.info(f"Successfully forwarded vitals to Cloud API for device {vitals.device_id}") return True except Exception as e: err_msg = str(e).strip() or "timeout / connection reset" - logger.warning(f"Error forwarding to Render Cloud: {type(e).__name__} ({err_msg})") + logger.warning(f"Error forwarding to Cloud API: {type(e).__name__} ({err_msg})") return False - results = await asyncio.gather(_forward_primary(), _forward_render(), return_exceptions=True) + results = await asyncio.gather(_forward_primary(), _forward_cloud(), return_exceptions=True) return any(r is True for r in results if not isinstance(r, Exception)) async def sync_patient_assignment_to_cloud(patient_data: dict, device_id: str) -> bool: """ - Synchronizes patient demographics assignment to Render Cloud Server. + Synchronizes patient demographics assignment to Cloud Server. Ensures local and remote dashboards always show the exact same patient. """ - is_on_render = os.getenv("RENDER") is not None or os.getenv("PORT") == "10000" or "render.local" in os.getenv("HOSTNAME", "") - if is_on_render: + cloud_base = get_cloud_base_url() + is_on_cloud = os.getenv("RENDER") is not None or os.getenv("PORT") == "10000" or "render.local" in os.getenv("HOSTNAME", "") + if is_on_cloud and ("onrender.com" in cloud_base): return True client = get_http_client() - render_assign_url = "https://contectfiveparamonitor.onrender.com/api/patient/assign" + cloud_assign_url = f"{cloud_base}/api/patient/assign" payload = { "device_id": device_id, "mrn": patient_data.get("mrn") or patient_data.get("patient_id") or "", @@ -108,13 +114,13 @@ async def sync_patient_assignment_to_cloud(patient_data: dict, device_id: str) - "patient_type": patient_data.get("patient_type") } try: - url_with_key = render_assign_url + url_with_key = cloud_assign_url if settings.api_token: - url_with_key = f"{render_assign_url}?api_key={settings.api_token}" + url_with_key = f"{cloud_assign_url}?api_key={settings.api_token}" resp = await client.post(url_with_key, json=payload, headers={"Content-Type": "application/json"}) resp.raise_for_status() - logger.info(f"Successfully synced patient {payload['mrn']} ({payload['name']}) to Render Cloud for {device_id}") + logger.info(f"Successfully synced patient {payload['mrn']} ({payload['name']}) to Cloud for {device_id}") return True except Exception as e: - logger.warning(f"Could not sync patient assignment to Render Cloud: {type(e).__name__} ({e})") + logger.warning(f"Could not sync patient assignment to Cloud: {type(e).__name__} ({e})") return False diff --git a/config.json b/config.json index 115508f..0d0712d 100644 --- a/config.json +++ b/config.json @@ -7,7 +7,8 @@ "device_models": { "192.168.100.120": "CMS7000PLUS" }, - "target_api_url": "https://innov-dev.beta.injomo.com/workflow.trigger/69b2712a7866bf86ca0060c3", + "target_api_url": "https://services-api.a2zhealth.in/v1/contec-monitor", + "cloud_api_url": "https://services-api.a2zhealth.in/v1/contec-monitor", "api_token": "", "retry_interval_seconds": 60, "log_file": "hl7_forwarder.log", diff --git a/config.py b/config.py index eb095e8..868942c 100644 --- a/config.py +++ b/config.py @@ -9,7 +9,8 @@ class Settings(BaseSettings): monitor_ip: str = "" monitor_model: str = "CMS7000PLUS" device_models: dict = {} - target_api_url: str = "https://api.example.com/vitals" + target_api_url: str = "https://services-api.a2zhealth.in/v1/contec-monitor" + cloud_api_url: str = "https://services-api.a2zhealth.in/v1/contec-monitor" api_token: str = "" retry_interval_seconds: int = 60 log_file: str = "hl7_forwarder.log" diff --git a/fiveparaminte-main/api_client.py b/fiveparaminte-main/api_client.py index 16c29fd..2a3bfb3 100644 --- a/fiveparaminte-main/api_client.py +++ b/fiveparaminte-main/api_client.py @@ -19,10 +19,14 @@ def get_http_client() -> httpx.AsyncClient: ) return _http_client +def get_cloud_base_url() -> str: + url = getattr(settings, "cloud_api_url", None) or os.getenv("CLOUD_API_URL") or "https://services-api.a2zhealth.in/v1/contec-monitor" + return url.rstrip("/") + async def forward_vitals_to_api(vitals: NormalizedVitals) -> bool: """ Attempts to POST the normalized vitals to the configured target API - and to the Render cloud database concurrently. + and to the Cloud server concurrently. Returns True if at least one forward succeeds. """ headers = {"Content-Type": "application/json"} @@ -59,16 +63,17 @@ async def forward_vitals_to_api(vitals: NormalizedVitals) -> bool: logger.warning(f"Error forwarding to primary API: {type(e).__name__} ({err_msg})") return False - # 2. Forward to Render Cloud Server - async def _forward_render() -> bool: - render_url = "https://contectfiveparamonitor.onrender.com/api/push-vitals" - is_on_render = os.getenv("RENDER") is not None or os.getenv("PORT") == "10000" or "render.local" in os.getenv("HOSTNAME", "") - if is_on_render: + # 2. Forward to Cloud Server + async def _forward_cloud() -> bool: + cloud_base = get_cloud_base_url() + cloud_url = f"{cloud_base}/api/push-vitals" + is_on_cloud = os.getenv("RENDER") is not None or os.getenv("PORT") == "10000" or "render.local" in os.getenv("HOSTNAME", "") + if is_on_cloud and ("onrender.com" in cloud_base): return True try: - url_with_key = render_url + url_with_key = cloud_url if settings.api_token: - url_with_key = f"{render_url}?api_key={settings.api_token}" + url_with_key = f"{cloud_url}?api_key={settings.api_token}" response = await client.post( url_with_key, @@ -76,28 +81,29 @@ async def forward_vitals_to_api(vitals: NormalizedVitals) -> bool: headers=headers ) response.raise_for_status() - logger.info(f"Successfully forwarded vitals to Render Cloud for device {vitals.device_id}") + logger.info(f"Successfully forwarded vitals to Cloud API for device {vitals.device_id}") return True except Exception as e: err_msg = str(e).strip() or "timeout / connection reset" - logger.warning(f"Error forwarding to Render Cloud: {type(e).__name__} ({err_msg})") + logger.warning(f"Error forwarding to Cloud API: {type(e).__name__} ({err_msg})") return False - results = await asyncio.gather(_forward_primary(), _forward_render(), return_exceptions=True) + results = await asyncio.gather(_forward_primary(), _forward_cloud(), return_exceptions=True) return any(r is True for r in results if not isinstance(r, Exception)) async def sync_patient_assignment_to_cloud(patient_data: dict, device_id: str) -> bool: """ - Synchronizes patient demographics assignment to Render Cloud Server. + Synchronizes patient demographics assignment to Cloud Server. Ensures local and remote dashboards always show the exact same patient. """ - is_on_render = os.getenv("RENDER") is not None or os.getenv("PORT") == "10000" or "render.local" in os.getenv("HOSTNAME", "") - if is_on_render: + cloud_base = get_cloud_base_url() + is_on_cloud = os.getenv("RENDER") is not None or os.getenv("PORT") == "10000" or "render.local" in os.getenv("HOSTNAME", "") + if is_on_cloud and ("onrender.com" in cloud_base): return True client = get_http_client() - render_assign_url = "https://contectfiveparamonitor.onrender.com/api/patient/assign" + cloud_assign_url = f"{cloud_base}/api/patient/assign" payload = { "device_id": device_id, "mrn": patient_data.get("mrn") or patient_data.get("patient_id") or "", @@ -108,13 +114,13 @@ async def sync_patient_assignment_to_cloud(patient_data: dict, device_id: str) - "patient_type": patient_data.get("patient_type") } try: - url_with_key = render_assign_url + url_with_key = cloud_assign_url if settings.api_token: - url_with_key = f"{render_assign_url}?api_key={settings.api_token}" + url_with_key = f"{cloud_assign_url}?api_key={settings.api_token}" resp = await client.post(url_with_key, json=payload, headers={"Content-Type": "application/json"}) resp.raise_for_status() - logger.info(f"Successfully synced patient {payload['mrn']} ({payload['name']}) to Render Cloud for {device_id}") + logger.info(f"Successfully synced patient {payload['mrn']} ({payload['name']}) to Cloud for {device_id}") return True except Exception as e: - logger.warning(f"Could not sync patient assignment to Render Cloud: {type(e).__name__} ({e})") + logger.warning(f"Could not sync patient assignment to Cloud: {type(e).__name__} ({e})") return False diff --git a/fiveparaminte-main/config.json b/fiveparaminte-main/config.json index 115508f..0d0712d 100644 --- a/fiveparaminte-main/config.json +++ b/fiveparaminte-main/config.json @@ -7,7 +7,8 @@ "device_models": { "192.168.100.120": "CMS7000PLUS" }, - "target_api_url": "https://innov-dev.beta.injomo.com/workflow.trigger/69b2712a7866bf86ca0060c3", + "target_api_url": "https://services-api.a2zhealth.in/v1/contec-monitor", + "cloud_api_url": "https://services-api.a2zhealth.in/v1/contec-monitor", "api_token": "", "retry_interval_seconds": 60, "log_file": "hl7_forwarder.log", diff --git a/fiveparaminte-main/config.py b/fiveparaminte-main/config.py index eb095e8..868942c 100644 --- a/fiveparaminte-main/config.py +++ b/fiveparaminte-main/config.py @@ -9,7 +9,8 @@ class Settings(BaseSettings): monitor_ip: str = "" monitor_model: str = "CMS7000PLUS" device_models: dict = {} - target_api_url: str = "https://api.example.com/vitals" + target_api_url: str = "https://services-api.a2zhealth.in/v1/contec-monitor" + cloud_api_url: str = "https://services-api.a2zhealth.in/v1/contec-monitor" api_token: str = "" retry_interval_seconds: int = 60 log_file: str = "hl7_forwarder.log" diff --git a/fiveparaminte-main/main.py b/fiveparaminte-main/main.py index 146b0b4..bcd9872 100644 --- a/fiveparaminte-main/main.py +++ b/fiveparaminte-main/main.py @@ -127,6 +127,7 @@ async def lifespan(app: FastAPI): print(f" Contec Ports: {settings.contec_ports}") print(f" Monitor Model: {model}") print(f" API Target: {settings.target_api_url}") + print(f" Cloud Sync: {getattr(settings, 'cloud_api_url', settings.target_api_url)}") print(f" Database: {settings.database_url}") print(f" ---------------------------------------------------------") print(f" YOUR PC's IP ADDRESSES (configure on your monitor):") @@ -196,22 +197,31 @@ app.add_middleware( allow_headers=["*"], ) -# Include dashboard routes +# Include dashboard routes under multiple prefixes so reverse proxies (Nginx) +# work whether they strip /v1/contec-monitor or forward it as-is app.include_router(dashboard.router, prefix="/api") +app.include_router(dashboard.router) +app.include_router(dashboard.router, prefix="/v1/contec-monitor/api") +app.include_router(dashboard.router, prefix="/v1/contec-monitor") -# Also serve dashboard at root and /dashboard directly for convenience +# Also serve dashboard directly for convenience across various path styles from fastapi.responses import HTMLResponse @app.get("/", response_class=HTMLResponse) @app.get("/dashboard", response_class=HTMLResponse) +@app.get("/v1/contec-monitor", response_class=HTMLResponse) +@app.get("/v1/contec-monitor/dashboard", response_class=HTMLResponse) +@app.get("/v1/contec-monitor/api/dashboard", response_class=HTMLResponse) def serve_dashboard_root(): return dashboard.serve_dashboard() # Serve static folder from fastapi.staticfiles import StaticFiles app.mount("/static", StaticFiles(directory="static"), name="static") +app.mount("/v1/contec-monitor/static", StaticFiles(directory="static"), name="static_subpath") from fastapi.responses import Response @app.get("/favicon.ico", include_in_schema=False) +@app.get("/v1/contec-monitor/favicon.ico", include_in_schema=False) async def favicon(): return Response(status_code=204, media_type="image/x-icon") @@ -220,6 +230,9 @@ async def favicon(): # === WebSocket endpoint for live vitals === @app.websocket("/ws/vitals") +@app.websocket("/api/ws/vitals") +@app.websocket("/v1/contec-monitor/ws/vitals") +@app.websocket("/v1/contec-monitor/api/ws/vitals") async def websocket_vitals(websocket: WebSocket): manager = get_ws_manager() await manager.connect(websocket) diff --git a/fiveparaminte-main/static/dashboard.html b/fiveparaminte-main/static/dashboard.html index 7d8a19e..afc3b33 100644 --- a/fiveparaminte-main/static/dashboard.html +++ b/fiveparaminte-main/static/dashboard.html @@ -3432,9 +3432,16 @@ } // === Configuration === - const API_BASE = window.location.origin + '/api'; + function getAppPrefix() { + const p = window.location.pathname; + if (p.includes('/api/')) return p.substring(0, p.indexOf('/api/')); + if (p.includes('/dashboard')) return p.substring(0, p.indexOf('/dashboard')); + return ''; + } + const APP_PREFIX = getAppPrefix(); + const API_BASE = window.location.origin + APP_PREFIX + '/api'; const POLL_INTERVAL = 2000; // 2 seconds - const WS_URL = `${window.location.protocol === 'https:' ? 'wss:' : 'ws:'}//${window.location.host}/ws/vitals`; + const WS_URL = `${window.location.protocol === 'https:' ? 'wss:' : 'ws:'}//${window.location.host}${APP_PREFIX}/ws/vitals`; // === Vital ranges for status calculation === const VITAL_RANGES = { diff --git a/main.py b/main.py index 146b0b4..bcd9872 100644 --- a/main.py +++ b/main.py @@ -127,6 +127,7 @@ async def lifespan(app: FastAPI): print(f" Contec Ports: {settings.contec_ports}") print(f" Monitor Model: {model}") print(f" API Target: {settings.target_api_url}") + print(f" Cloud Sync: {getattr(settings, 'cloud_api_url', settings.target_api_url)}") print(f" Database: {settings.database_url}") print(f" ---------------------------------------------------------") print(f" YOUR PC's IP ADDRESSES (configure on your monitor):") @@ -196,22 +197,31 @@ app.add_middleware( allow_headers=["*"], ) -# Include dashboard routes +# Include dashboard routes under multiple prefixes so reverse proxies (Nginx) +# work whether they strip /v1/contec-monitor or forward it as-is app.include_router(dashboard.router, prefix="/api") +app.include_router(dashboard.router) +app.include_router(dashboard.router, prefix="/v1/contec-monitor/api") +app.include_router(dashboard.router, prefix="/v1/contec-monitor") -# Also serve dashboard at root and /dashboard directly for convenience +# Also serve dashboard directly for convenience across various path styles from fastapi.responses import HTMLResponse @app.get("/", response_class=HTMLResponse) @app.get("/dashboard", response_class=HTMLResponse) +@app.get("/v1/contec-monitor", response_class=HTMLResponse) +@app.get("/v1/contec-monitor/dashboard", response_class=HTMLResponse) +@app.get("/v1/contec-monitor/api/dashboard", response_class=HTMLResponse) def serve_dashboard_root(): return dashboard.serve_dashboard() # Serve static folder from fastapi.staticfiles import StaticFiles app.mount("/static", StaticFiles(directory="static"), name="static") +app.mount("/v1/contec-monitor/static", StaticFiles(directory="static"), name="static_subpath") from fastapi.responses import Response @app.get("/favicon.ico", include_in_schema=False) +@app.get("/v1/contec-monitor/favicon.ico", include_in_schema=False) async def favicon(): return Response(status_code=204, media_type="image/x-icon") @@ -220,6 +230,9 @@ async def favicon(): # === WebSocket endpoint for live vitals === @app.websocket("/ws/vitals") +@app.websocket("/api/ws/vitals") +@app.websocket("/v1/contec-monitor/ws/vitals") +@app.websocket("/v1/contec-monitor/api/ws/vitals") async def websocket_vitals(websocket: WebSocket): manager = get_ws_manager() await manager.connect(websocket) diff --git a/static/dashboard.html b/static/dashboard.html index 7d8a19e..afc3b33 100644 --- a/static/dashboard.html +++ b/static/dashboard.html @@ -3432,9 +3432,16 @@ } // === Configuration === - const API_BASE = window.location.origin + '/api'; + function getAppPrefix() { + const p = window.location.pathname; + if (p.includes('/api/')) return p.substring(0, p.indexOf('/api/')); + if (p.includes('/dashboard')) return p.substring(0, p.indexOf('/dashboard')); + return ''; + } + const APP_PREFIX = getAppPrefix(); + const API_BASE = window.location.origin + APP_PREFIX + '/api'; const POLL_INTERVAL = 2000; // 2 seconds - const WS_URL = `${window.location.protocol === 'https:' ? 'wss:' : 'ws:'}//${window.location.host}/ws/vitals`; + const WS_URL = `${window.location.protocol === 'https:' ? 'wss:' : 'ws:'}//${window.location.host}${APP_PREFIX}/ws/vitals`; // === Vital ranges for status calculation === const VITAL_RANGES = {