add configurable cloud api url and subpath routing support for reverse proxies
This commit is contained in:
+25
-19
@@ -19,10 +19,14 @@ def get_http_client() -> httpx.AsyncClient:
|
|||||||
)
|
)
|
||||||
return _http_client
|
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:
|
async def forward_vitals_to_api(vitals: NormalizedVitals) -> bool:
|
||||||
"""
|
"""
|
||||||
Attempts to POST the normalized vitals to the configured target API
|
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.
|
Returns True if at least one forward succeeds.
|
||||||
"""
|
"""
|
||||||
headers = {"Content-Type": "application/json"}
|
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})")
|
logger.warning(f"Error forwarding to primary API: {type(e).__name__} ({err_msg})")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# 2. Forward to Render Cloud Server
|
# 2. Forward to Cloud Server
|
||||||
async def _forward_render() -> bool:
|
async def _forward_cloud() -> bool:
|
||||||
render_url = "https://contectfiveparamonitor.onrender.com/api/push-vitals"
|
cloud_base = get_cloud_base_url()
|
||||||
is_on_render = os.getenv("RENDER") is not None or os.getenv("PORT") == "10000" or "render.local" in os.getenv("HOSTNAME", "")
|
cloud_url = f"{cloud_base}/api/push-vitals"
|
||||||
if is_on_render:
|
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
|
return True
|
||||||
try:
|
try:
|
||||||
url_with_key = render_url
|
url_with_key = cloud_url
|
||||||
if settings.api_token:
|
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(
|
response = await client.post(
|
||||||
url_with_key,
|
url_with_key,
|
||||||
@@ -76,28 +81,29 @@ async def forward_vitals_to_api(vitals: NormalizedVitals) -> bool:
|
|||||||
headers=headers
|
headers=headers
|
||||||
)
|
)
|
||||||
response.raise_for_status()
|
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
|
return True
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
err_msg = str(e).strip() or "timeout / connection reset"
|
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
|
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))
|
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:
|
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.
|
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", "")
|
cloud_base = get_cloud_base_url()
|
||||||
if is_on_render:
|
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
|
return True
|
||||||
|
|
||||||
client = get_http_client()
|
client = get_http_client()
|
||||||
render_assign_url = "https://contectfiveparamonitor.onrender.com/api/patient/assign"
|
cloud_assign_url = f"{cloud_base}/api/patient/assign"
|
||||||
payload = {
|
payload = {
|
||||||
"device_id": device_id,
|
"device_id": device_id,
|
||||||
"mrn": patient_data.get("mrn") or patient_data.get("patient_id") or "",
|
"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")
|
"patient_type": patient_data.get("patient_type")
|
||||||
}
|
}
|
||||||
try:
|
try:
|
||||||
url_with_key = render_assign_url
|
url_with_key = cloud_assign_url
|
||||||
if settings.api_token:
|
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 = await client.post(url_with_key, json=payload, headers={"Content-Type": "application/json"})
|
||||||
resp.raise_for_status()
|
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
|
return True
|
||||||
except Exception as e:
|
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
|
return False
|
||||||
|
|||||||
+2
-1
@@ -7,7 +7,8 @@
|
|||||||
"device_models": {
|
"device_models": {
|
||||||
"192.168.100.120": "CMS7000PLUS"
|
"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": "",
|
"api_token": "",
|
||||||
"retry_interval_seconds": 60,
|
"retry_interval_seconds": 60,
|
||||||
"log_file": "hl7_forwarder.log",
|
"log_file": "hl7_forwarder.log",
|
||||||
|
|||||||
@@ -9,7 +9,8 @@ class Settings(BaseSettings):
|
|||||||
monitor_ip: str = ""
|
monitor_ip: str = ""
|
||||||
monitor_model: str = "CMS7000PLUS"
|
monitor_model: str = "CMS7000PLUS"
|
||||||
device_models: dict = {}
|
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 = ""
|
api_token: str = ""
|
||||||
retry_interval_seconds: int = 60
|
retry_interval_seconds: int = 60
|
||||||
log_file: str = "hl7_forwarder.log"
|
log_file: str = "hl7_forwarder.log"
|
||||||
|
|||||||
@@ -19,10 +19,14 @@ def get_http_client() -> httpx.AsyncClient:
|
|||||||
)
|
)
|
||||||
return _http_client
|
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:
|
async def forward_vitals_to_api(vitals: NormalizedVitals) -> bool:
|
||||||
"""
|
"""
|
||||||
Attempts to POST the normalized vitals to the configured target API
|
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.
|
Returns True if at least one forward succeeds.
|
||||||
"""
|
"""
|
||||||
headers = {"Content-Type": "application/json"}
|
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})")
|
logger.warning(f"Error forwarding to primary API: {type(e).__name__} ({err_msg})")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# 2. Forward to Render Cloud Server
|
# 2. Forward to Cloud Server
|
||||||
async def _forward_render() -> bool:
|
async def _forward_cloud() -> bool:
|
||||||
render_url = "https://contectfiveparamonitor.onrender.com/api/push-vitals"
|
cloud_base = get_cloud_base_url()
|
||||||
is_on_render = os.getenv("RENDER") is not None or os.getenv("PORT") == "10000" or "render.local" in os.getenv("HOSTNAME", "")
|
cloud_url = f"{cloud_base}/api/push-vitals"
|
||||||
if is_on_render:
|
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
|
return True
|
||||||
try:
|
try:
|
||||||
url_with_key = render_url
|
url_with_key = cloud_url
|
||||||
if settings.api_token:
|
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(
|
response = await client.post(
|
||||||
url_with_key,
|
url_with_key,
|
||||||
@@ -76,28 +81,29 @@ async def forward_vitals_to_api(vitals: NormalizedVitals) -> bool:
|
|||||||
headers=headers
|
headers=headers
|
||||||
)
|
)
|
||||||
response.raise_for_status()
|
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
|
return True
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
err_msg = str(e).strip() or "timeout / connection reset"
|
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
|
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))
|
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:
|
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.
|
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", "")
|
cloud_base = get_cloud_base_url()
|
||||||
if is_on_render:
|
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
|
return True
|
||||||
|
|
||||||
client = get_http_client()
|
client = get_http_client()
|
||||||
render_assign_url = "https://contectfiveparamonitor.onrender.com/api/patient/assign"
|
cloud_assign_url = f"{cloud_base}/api/patient/assign"
|
||||||
payload = {
|
payload = {
|
||||||
"device_id": device_id,
|
"device_id": device_id,
|
||||||
"mrn": patient_data.get("mrn") or patient_data.get("patient_id") or "",
|
"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")
|
"patient_type": patient_data.get("patient_type")
|
||||||
}
|
}
|
||||||
try:
|
try:
|
||||||
url_with_key = render_assign_url
|
url_with_key = cloud_assign_url
|
||||||
if settings.api_token:
|
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 = await client.post(url_with_key, json=payload, headers={"Content-Type": "application/json"})
|
||||||
resp.raise_for_status()
|
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
|
return True
|
||||||
except Exception as e:
|
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
|
return False
|
||||||
|
|||||||
@@ -7,7 +7,8 @@
|
|||||||
"device_models": {
|
"device_models": {
|
||||||
"192.168.100.120": "CMS7000PLUS"
|
"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": "",
|
"api_token": "",
|
||||||
"retry_interval_seconds": 60,
|
"retry_interval_seconds": 60,
|
||||||
"log_file": "hl7_forwarder.log",
|
"log_file": "hl7_forwarder.log",
|
||||||
|
|||||||
@@ -9,7 +9,8 @@ class Settings(BaseSettings):
|
|||||||
monitor_ip: str = ""
|
monitor_ip: str = ""
|
||||||
monitor_model: str = "CMS7000PLUS"
|
monitor_model: str = "CMS7000PLUS"
|
||||||
device_models: dict = {}
|
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 = ""
|
api_token: str = ""
|
||||||
retry_interval_seconds: int = 60
|
retry_interval_seconds: int = 60
|
||||||
log_file: str = "hl7_forwarder.log"
|
log_file: str = "hl7_forwarder.log"
|
||||||
|
|||||||
@@ -127,6 +127,7 @@ async def lifespan(app: FastAPI):
|
|||||||
print(f" Contec Ports: {settings.contec_ports}")
|
print(f" Contec Ports: {settings.contec_ports}")
|
||||||
print(f" Monitor Model: {model}")
|
print(f" Monitor Model: {model}")
|
||||||
print(f" API Target: {settings.target_api_url}")
|
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" Database: {settings.database_url}")
|
||||||
print(f" ---------------------------------------------------------")
|
print(f" ---------------------------------------------------------")
|
||||||
print(f" YOUR PC's IP ADDRESSES (configure on your monitor):")
|
print(f" YOUR PC's IP ADDRESSES (configure on your monitor):")
|
||||||
@@ -196,22 +197,31 @@ app.add_middleware(
|
|||||||
allow_headers=["*"],
|
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, 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
|
from fastapi.responses import HTMLResponse
|
||||||
@app.get("/", response_class=HTMLResponse)
|
@app.get("/", response_class=HTMLResponse)
|
||||||
@app.get("/dashboard", 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():
|
def serve_dashboard_root():
|
||||||
return dashboard.serve_dashboard()
|
return dashboard.serve_dashboard()
|
||||||
|
|
||||||
# Serve static folder
|
# Serve static folder
|
||||||
from fastapi.staticfiles import StaticFiles
|
from fastapi.staticfiles import StaticFiles
|
||||||
app.mount("/static", StaticFiles(directory="static"), name="static")
|
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
|
from fastapi.responses import Response
|
||||||
@app.get("/favicon.ico", include_in_schema=False)
|
@app.get("/favicon.ico", include_in_schema=False)
|
||||||
|
@app.get("/v1/contec-monitor/favicon.ico", include_in_schema=False)
|
||||||
async def favicon():
|
async def favicon():
|
||||||
return Response(status_code=204, media_type="image/x-icon")
|
return Response(status_code=204, media_type="image/x-icon")
|
||||||
|
|
||||||
@@ -220,6 +230,9 @@ async def favicon():
|
|||||||
|
|
||||||
# === WebSocket endpoint for live vitals ===
|
# === WebSocket endpoint for live vitals ===
|
||||||
@app.websocket("/ws/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):
|
async def websocket_vitals(websocket: WebSocket):
|
||||||
manager = get_ws_manager()
|
manager = get_ws_manager()
|
||||||
await manager.connect(websocket)
|
await manager.connect(websocket)
|
||||||
|
|||||||
@@ -3432,9 +3432,16 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// === Configuration ===
|
// === 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 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 ===
|
// === Vital ranges for status calculation ===
|
||||||
const VITAL_RANGES = {
|
const VITAL_RANGES = {
|
||||||
|
|||||||
@@ -127,6 +127,7 @@ async def lifespan(app: FastAPI):
|
|||||||
print(f" Contec Ports: {settings.contec_ports}")
|
print(f" Contec Ports: {settings.contec_ports}")
|
||||||
print(f" Monitor Model: {model}")
|
print(f" Monitor Model: {model}")
|
||||||
print(f" API Target: {settings.target_api_url}")
|
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" Database: {settings.database_url}")
|
||||||
print(f" ---------------------------------------------------------")
|
print(f" ---------------------------------------------------------")
|
||||||
print(f" YOUR PC's IP ADDRESSES (configure on your monitor):")
|
print(f" YOUR PC's IP ADDRESSES (configure on your monitor):")
|
||||||
@@ -196,22 +197,31 @@ app.add_middleware(
|
|||||||
allow_headers=["*"],
|
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, 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
|
from fastapi.responses import HTMLResponse
|
||||||
@app.get("/", response_class=HTMLResponse)
|
@app.get("/", response_class=HTMLResponse)
|
||||||
@app.get("/dashboard", 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():
|
def serve_dashboard_root():
|
||||||
return dashboard.serve_dashboard()
|
return dashboard.serve_dashboard()
|
||||||
|
|
||||||
# Serve static folder
|
# Serve static folder
|
||||||
from fastapi.staticfiles import StaticFiles
|
from fastapi.staticfiles import StaticFiles
|
||||||
app.mount("/static", StaticFiles(directory="static"), name="static")
|
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
|
from fastapi.responses import Response
|
||||||
@app.get("/favicon.ico", include_in_schema=False)
|
@app.get("/favicon.ico", include_in_schema=False)
|
||||||
|
@app.get("/v1/contec-monitor/favicon.ico", include_in_schema=False)
|
||||||
async def favicon():
|
async def favicon():
|
||||||
return Response(status_code=204, media_type="image/x-icon")
|
return Response(status_code=204, media_type="image/x-icon")
|
||||||
|
|
||||||
@@ -220,6 +230,9 @@ async def favicon():
|
|||||||
|
|
||||||
# === WebSocket endpoint for live vitals ===
|
# === WebSocket endpoint for live vitals ===
|
||||||
@app.websocket("/ws/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):
|
async def websocket_vitals(websocket: WebSocket):
|
||||||
manager = get_ws_manager()
|
manager = get_ws_manager()
|
||||||
await manager.connect(websocket)
|
await manager.connect(websocket)
|
||||||
|
|||||||
@@ -3432,9 +3432,16 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// === Configuration ===
|
// === 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 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 ===
|
// === Vital ranges for status calculation ===
|
||||||
const VITAL_RANGES = {
|
const VITAL_RANGES = {
|
||||||
|
|||||||
Reference in New Issue
Block a user