2026-07-08 14:14:03 +05:30
<!DOCTYPE html>
< html lang = "en" >
< head >
< meta charset = "UTF-8" >
< meta name = "viewport" content = "width=device-width, initial-scale=1.0" >
2026-07-11 14:24:14 +05:30
< title > Patient Vital Signs Monitor — Contec CMS7000PLUS / CMS8500</ title >
< meta name = "description" content = "Real-time patient vital signs monitoring dashboard for Contec CMS7000PLUS / CMS8500 patient monitors" >
2026-07-08 14:14:03 +05:30
< link rel = "preconnect" href = "https://fonts.googleapis.com" >
< link rel = "preconnect" href = "https://fonts.gstatic.com" crossorigin >
2026-07-13 15:17:30 +05:30
< link href = "https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700;800;900&family=JetBrains+Mono:wght@400;600;700&display=swap" rel = "stylesheet" >
2026-07-08 14:14:03 +05:30
< style >
/* === CSS Reset & Base === */
*, * :: before , * :: after {
margin : 0 ;
padding : 0 ;
box-sizing : border-box ;
}
: root {
2026-07-13 15:17:30 +05:30
/* Core palette (Light White Theme) */
--bg-primary : #f8fafc ;
--bg-secondary : #ffffff ;
--bg-card : #ffffff ;
--bg-card-hover : #f1f5f9 ;
--border-color : rgba ( 15 , 23 , 42 , 0.08 );
--text-primary : #0f172a ;
--text-secondary : #475569 ;
--text-dim : #94a3b8 ;
2026-07-08 14:14:03 +05:30
2026-07-13 15:17:30 +05:30
/* Vital sign accent colors (optimized for white background contrast) */
--hr-color : #dc2626 ; /* dark red */
--hr-glow : rgba ( 220 , 38 , 38 , 0.1 );
--spo2-color : #0891b2 ; /* dark cyan */
--spo2-glow : rgba ( 8 , 145 , 178 , 0.1 );
--bp-color : #7c3aed ; /* dark purple */
--bp-glow : rgba ( 124 , 58 , 237 , 0.1 );
--rr-color : #16a34a ; /* dark green */
--rr-glow : rgba ( 22 , 163 , 74 , 0.1 );
--temp-color : #ea580c ; /* dark orange */
--temp-glow : rgba ( 234 , 88 , 12 , 0.1 );
--map-color : #db2777 ; /* dark pink */
--map-glow : rgba ( 219 , 39 , 119 , 0.1 );
2026-07-08 14:14:03 +05:30
2026-07-13 15:17:30 +05:30
/* Status colors */
--status-ok : #16a34a ;
--status-warn : #ca8a04 ;
--status-critical : #dc2626 ;
2026-07-08 14:14:03 +05:30
--status-offline : #64748b ;
}
body {
2026-07-13 15:17:30 +05:30
font-family : 'Outfit' , - apple-system , BlinkMacSystemFont , 'Segoe UI' , Roboto , sans-serif ;
2026-07-08 14:14:03 +05:30
background : var ( -- bg - primary );
color : var ( -- text - primary );
min-height : 100 vh ;
overflow-x : hidden ;
2026-07-13 15:17:30 +05:30
display : flex ;
flex-direction : column ;
2026-07-08 14:14:03 +05:30
}
2026-07-13 15:17:30 +05:30
/* === Animated Grid Background === */
2026-07-08 14:14:03 +05:30
. bg-grid {
position : fixed ;
inset : 0 ;
z-index : 0 ;
background-image :
2026-07-13 15:17:30 +05:30
linear-gradient ( rgba ( 15 , 23 , 42 , 0.03 ) 1 px , transparent 1 px ),
linear-gradient ( 90 deg , rgba ( 15 , 23 , 42 , 0.03 ) 1 px , transparent 1 px );
background-size : 30 px 30 px ;
2026-07-08 14:14:03 +05:30
pointer-events : none ;
}
. bg-glow {
position : fixed ;
top : -200 px ;
left : 50 % ;
transform : translateX ( -50 % );
width : 800 px ;
height : 600 px ;
2026-07-13 15:17:30 +05:30
background : radial-gradient ( ellipse , rgba ( 8 , 145 , 178 , 0.05 ), transparent 70 % );
2026-07-08 14:14:03 +05:30
z-index : 0 ;
pointer-events : none ;
animation : glowPulse 8 s ease-in-out infinite ;
}
@ keyframes glowPulse {
0 %, 100 % { opacity : 0.5 ; transform : translateX ( -50 % ) scale ( 1 ); }
50 % { opacity : 1 ; transform : translateX ( -50 % ) scale ( 1.1 ); }
}
/* === Layout === */
. app-container {
position : relative ;
z-index : 1 ;
2026-07-13 16:35:33 +05:30
max-width : 1400 px ;
margin : 0 auto ;
2026-07-13 16:52:41 +05:30
padding : 12 px ;
2026-07-13 16:35:33 +05:30
min-height : 100 vh ;
2026-07-08 14:14:03 +05:30
display : flex ;
flex-direction : column ;
2026-07-13 16:52:41 +05:30
justify-content : flex-start ;
2026-07-13 15:17:30 +05:30
width : 100 % ;
2026-07-13 16:52:41 +05:30
box-sizing : border-box ;
2026-07-08 14:14:03 +05:30
}
/* === Header === */
. header {
display : flex ;
align-items : center ;
justify-content : space-between ;
2026-07-13 16:24:37 +05:30
padding : 8 px 16 px ;
2026-07-08 14:14:03 +05:30
background : var ( -- bg - card );
border : 1 px solid var ( -- border - color );
2026-07-13 16:24:37 +05:30
border-radius : 12 px ;
2026-07-13 15:17:30 +05:30
box-shadow :
0 2 px 4 px -1 px rgba ( 0 , 0 , 0 , 0.01 ),
2026-07-13 16:24:37 +05:30
0 4 px 6 px -3 px rgba ( 0 , 0 , 0 , 0.02 );
margin-bottom : 12 px ;
2026-07-08 14:14:03 +05:30
}
. header-left {
display : flex ;
align-items : center ;
2026-07-13 16:24:37 +05:30
gap : 10 px ;
2026-07-08 14:14:03 +05:30
}
. header-icon {
2026-07-13 16:24:37 +05:30
width : 32 px ;
height : 32 px ;
border-radius : 8 px ;
2026-07-08 14:14:03 +05:30
background : linear-gradient ( 135 deg , var ( -- spo2 - color ), var ( -- hr - color ));
display : flex ;
align-items : center ;
justify-content : center ;
2026-07-13 16:24:37 +05:30
font-size : 16 px ;
box-shadow : 0 2 px 6 px rgba ( 8 , 145 , 178 , 0.15 );
2026-07-08 14:14:03 +05:30
}
. header-title {
2026-07-13 16:24:37 +05:30
font-size : 15 px ;
2026-07-08 14:14:03 +05:30
font-weight : 700 ;
2026-07-13 16:24:37 +05:30
letter-spacing : -0.3 px ;
2026-07-13 15:17:30 +05:30
color : var ( -- text - primary );
2026-07-08 14:14:03 +05:30
}
. header-subtitle {
2026-07-13 16:24:37 +05:30
font-size : 11 px ;
2026-07-08 14:14:03 +05:30
color : var ( -- text - secondary );
2026-07-13 15:17:30 +05:30
margin-top : 1 px ;
font-weight : 500 ;
2026-07-08 14:14:03 +05:30
}
. header-right {
display : flex ;
align-items : center ;
2026-07-13 16:24:37 +05:30
gap : 12 px ;
2026-07-08 14:14:03 +05:30
}
/* Audio Alert Toggle */
. audio-toggle {
display : flex ;
align-items : center ;
2026-07-13 16:24:37 +05:30
gap : 6 px ;
padding : 6 px 12 px ;
border-radius : 12 px ;
font-size : 11 px ;
2026-07-13 15:17:30 +05:30
font-weight : 600 ;
background : rgba ( 71 , 85 , 105 , 0.06 );
border : 1 px solid rgba ( 71 , 85 , 105 , 0.1 );
2026-07-08 14:14:03 +05:30
color : var ( -- text - secondary );
cursor : pointer ;
2026-07-13 15:17:30 +05:30
transition : all 0.3 s cubic-bezier ( 0.16 , 1 , 0.3 , 1 );
2026-07-08 14:14:03 +05:30
user-select : none ;
}
. audio-toggle : hover {
2026-07-13 15:17:30 +05:30
background : rgba ( 71 , 85 , 105 , 0.12 );
color : var ( -- text - primary );
2026-07-08 14:14:03 +05:30
}
. audio-toggle . active {
2026-07-13 15:17:30 +05:30
background : rgba ( 239 , 68 , 68 , 0.08 );
border-color : rgba ( 239 , 68 , 68 , 0.2 );
2026-07-08 14:14:03 +05:30
color : var ( -- status - critical );
}
. connection-badge {
display : flex ;
align-items : center ;
2026-07-13 16:24:37 +05:30
gap : 6 px ;
padding : 6 px 12 px ;
border-radius : 12 px ;
font-size : 11 px ;
2026-07-13 15:17:30 +05:30
font-weight : 600 ;
background : rgba ( 22 , 163 , 74 , 0.08 );
border : 1 px solid rgba ( 22 , 163 , 74 , 0.15 );
2026-07-08 14:14:03 +05:30
color : var ( -- status - ok );
transition : all 0.3 s ;
}
. connection-badge . offline {
2026-07-13 15:17:30 +05:30
background : rgba ( 100 , 116 , 139 , 0.08 );
border-color : rgba ( 100 , 116 , 139 , 0.15 );
2026-07-08 14:14:03 +05:30
color : var ( -- status - offline );
}
. connection-badge . warning {
2026-07-13 15:17:30 +05:30
background : rgba ( 202 , 138 , 4 , 0.08 );
border-color : rgba ( 202 , 138 , 4 , 0.15 );
2026-07-08 14:14:03 +05:30
color : var ( -- status - warn );
}
. status-dot {
2026-07-13 16:24:37 +05:30
width : 7 px ;
height : 7 px ;
2026-07-08 14:14:03 +05:30
border-radius : 50 % ;
background : var ( -- status - ok );
animation : statusPulse 2 s ease-in-out infinite ;
}
. connection-badge . offline . status-dot {
background : var ( -- status - offline );
animation : none ;
}
@ keyframes statusPulse {
0 %, 100 % { opacity : 1 ; box-shadow : 0 0 0 0 rgba ( 34 , 197 , 94 , 0.4 ); }
2026-07-13 16:24:37 +05:30
50 % { opacity : 0.7 ; box-shadow : 0 0 0 5 px rgba ( 34 , 197 , 94 , 0 ); }
2026-07-08 14:14:03 +05:30
}
. time-display {
font-family : 'JetBrains Mono' , monospace ;
2026-07-13 16:24:37 +05:30
font-size : 12 px ;
2026-07-13 15:17:30 +05:30
font-weight : 600 ;
2026-07-08 14:14:03 +05:30
color : var ( -- text - secondary );
}
2026-07-11 14:24:14 +05:30
/* === Device Selector Styles === */
. device-selector-container {
display : flex ;
align-items : center ;
2026-07-13 16:24:37 +05:30
gap : 6 px ;
margin-right : 4 px ;
2026-07-11 14:24:14 +05:30
}
. device-select-dropdown {
2026-07-13 15:17:30 +05:30
background : #ffffff ;
2026-07-11 14:24:14 +05:30
border : 1 px solid var ( -- border - color );
color : var ( -- text - primary );
2026-07-13 16:24:37 +05:30
padding : 6 px 12 px ;
border-radius : 12 px ;
font-size : 11 px ;
2026-07-13 15:17:30 +05:30
font-weight : 600 ;
2026-07-11 14:24:14 +05:30
outline : none ;
cursor : pointer ;
2026-07-13 16:24:37 +05:30
box-shadow : 0 1 px 3 px rgba ( 0 , 0 , 0 , 0.01 );
2026-07-11 14:24:14 +05:30
transition : all 0.3 s ;
}
. device-select-dropdown : hover {
border-color : var ( -- spo2 - color );
2026-07-13 15:17:30 +05:30
background : #f8fafc ;
2026-07-11 14:24:14 +05:30
}
. device-select-dropdown option {
2026-07-13 15:17:30 +05:30
background-color : #ffffff ;
2026-07-11 14:24:14 +05:30
color : var ( -- text - primary );
}
2026-07-08 14:14:03 +05:30
/* === Setup Guide Panel === */
. setup-guide {
2026-07-13 16:24:37 +05:30
display : none !important ;
2026-07-08 14:14:03 +05:30
}
. setup-guide-header {
display : flex ;
align-items : center ;
justify-content : space-between ;
2026-07-13 15:17:30 +05:30
padding : 16 px 24 px ;
2026-07-08 14:14:03 +05:30
cursor : pointer ;
user-select : none ;
}
. setup-guide-header : hover {
2026-07-13 15:17:30 +05:30
background : rgba ( 0 , 0 , 0 , 0.01 );
2026-07-08 14:14:03 +05:30
}
. setup-guide-title {
display : flex ;
align-items : center ;
gap : 12 px ;
2026-07-13 15:17:30 +05:30
font-size : 14 px ;
font-weight : 700 ;
2026-07-08 14:14:03 +05:30
color : var ( -- spo2 - color );
}
. setup-guide-toggle {
font-size : 12 px ;
2026-07-13 15:17:30 +05:30
color : var ( -- text - secondary );
2026-07-08 14:14:03 +05:30
padding : 4 px 12 px ;
border-radius : 8 px ;
2026-07-13 15:17:30 +05:30
background : rgba ( 0 , 0 , 0 , 0.03 );
2026-07-08 14:14:03 +05:30
transition : all 0.2 s ;
2026-07-13 15:17:30 +05:30
font-weight : 600 ;
2026-07-08 14:14:03 +05:30
}
. setup-guide-body {
padding : 0 24 px 20 px 24 px ;
display : grid ;
grid-template-columns : 1 fr 1 fr ;
gap : 20 px ;
}
. setup-step {
display : flex ;
gap : 14 px ;
align-items : flex-start ;
}
. step-number {
width : 32 px ;
height : 32 px ;
border-radius : 10 px ;
2026-07-13 15:17:30 +05:30
background : rgba ( 8 , 145 , 178 , 0.08 );
2026-07-08 14:14:03 +05:30
color : var ( -- spo2 - color );
display : flex ;
align-items : center ;
justify-content : center ;
font-weight : 700 ;
font-size : 14 px ;
flex-shrink : 0 ;
}
. step-content h4 {
font-size : 13 px ;
2026-07-13 15:17:30 +05:30
font-weight : 700 ;
2026-07-08 14:14:03 +05:30
color : var ( -- text - primary );
margin-bottom : 4 px ;
}
. step-content p {
font-size : 12 px ;
color : var ( -- text - secondary );
line-height : 1.5 ;
}
. step-content code {
2026-07-13 15:17:30 +05:30
background : rgba ( 0 , 0 , 0 , 0.04 );
2026-07-08 14:14:03 +05:30
padding : 2 px 6 px ;
border-radius : 4 px ;
font-family : 'JetBrains Mono' , monospace ;
font-size : 11 px ;
color : var ( -- spo2 - color );
}
. setup-ip-display {
grid-column : 1 / -1 ;
2026-07-13 15:17:30 +05:30
background : rgba ( 0 , 0 , 0 , 0.02 );
2026-07-08 14:14:03 +05:30
border-radius : 12 px ;
padding : 16 px 20 px ;
display : flex ;
align-items : center ;
justify-content : space-between ;
flex-wrap : wrap ;
gap : 12 px ;
2026-07-13 15:17:30 +05:30
border : 1 px solid rgba ( 0 , 0 , 0 , 0.04 );
}
. setup-ip-display div {
font-weight : 600 ;
2026-07-08 14:14:03 +05:30
}
. setup-ip-label {
font-size : 13 px ;
color : var ( -- text - secondary );
2026-07-13 15:17:30 +05:30
font-weight : 600 ;
2026-07-08 14:14:03 +05:30
}
. setup-ip-value {
font-family : 'JetBrains Mono' , monospace ;
font-size : 18 px ;
2026-07-13 15:17:30 +05:30
font-weight : 800 ;
2026-07-08 14:14:03 +05:30
color : var ( -- spo2 - color );
}
/* === Vitals Grid === */
. vitals-grid {
display : grid ;
grid-template-columns : repeat ( 3 , 1 fr );
2026-07-13 16:44:54 +05:30
gap : 10 px ;
2026-07-13 16:52:41 +05:30
align-content : start ;
2026-07-08 14:14:03 +05:30
}
. vital-card {
background : var ( -- bg - card );
border : 1 px solid var ( -- border - color );
2026-07-13 16:42:16 +05:30
border-radius : 8 px ;
2026-07-13 16:52:41 +05:30
padding : 8 px 12 px ;
2026-07-08 14:14:03 +05:30
position : relative ;
overflow : hidden ;
2026-07-13 15:17:30 +05:30
box-shadow :
2026-07-13 16:44:54 +05:30
0 1 px 3 px rgba ( 0 , 0 , 0 , 0.01 ),
0 2 px 4 px rgba ( 0 , 0 , 0 , 0.02 );
2026-07-08 14:14:03 +05:30
transition : all 0.4 s cubic-bezier ( 0.16 , 1 , 0.3 , 1 );
2026-07-13 16:35:33 +05:30
display : grid ;
2026-07-13 16:44:54 +05:30
grid-template-areas : "header value waveform" ;
2026-07-13 16:52:41 +05:30
grid-template-columns : 90 px 70 px 1 fr ;
grid-template-rows : 40 px ;
2026-07-13 16:44:54 +05:30
align-items : center ;
2026-07-13 16:52:41 +05:30
gap : 0 10 px ;
height : 56 px ;
box-sizing : border-box ;
2026-07-08 14:14:03 +05:30
}
. vital-card : hover {
background : var ( -- bg - card - hover );
2026-07-13 16:42:16 +05:30
transform : translateY ( -1 px );
2026-07-13 15:17:30 +05:30
box-shadow :
2026-07-13 16:44:54 +05:30
0 4 px 8 px -2 px rgba ( 0 , 0 , 0 , 0.04 ),
2026-07-13 16:20:30 +05:30
0 0 0 1 px rgba ( 0 , 0 , 0 , 0.01 );
2026-07-08 14:14:03 +05:30
}
2026-07-13 16:20:30 +05:30
. vital-card . hr : hover { border-color : rgba ( 220 , 38 , 38 , 0.25 ); }
. vital-card . spo2 : hover { border-color : rgba ( 8 , 145 , 178 , 0.25 ); }
. vital-card . bp : hover { border-color : rgba ( 124 , 58 , 237 , 0.25 ); }
. vital-card . rr : hover { border-color : rgba ( 22 , 163 , 74 , 0.25 ); }
. vital-card . temp : hover { border-color : rgba ( 234 , 88 , 12 , 0.25 ); }
. vital-card . map-card : hover { border-color : rgba ( 219 , 39 , 119 , 0.25 ); }
2026-07-13 15:23:10 +05:30
2026-07-08 14:14:03 +05:30
. vital-card :: before {
content : '' ;
position : absolute ;
top : 0 ;
left : 0 ;
2026-07-13 16:35:33 +05:30
bottom : 0 ;
2026-07-13 16:42:16 +05:30
width : 3 px ;
border-radius : 8 px 0 0 8 px ;
2026-07-08 14:14:03 +05:30
}
2026-07-13 16:35:33 +05:30
. vital-card . hr :: before { background : linear-gradient ( 180 deg , var ( -- hr - color ), transparent ); }
. vital-card . spo2 :: before { background : linear-gradient ( 180 deg , var ( -- spo2 - color ), transparent ); }
. vital-card . bp :: before { background : linear-gradient ( 180 deg , var ( -- bp - color ), transparent ); }
. vital-card . rr :: before { background : linear-gradient ( 180 deg , var ( -- rr - color ), transparent ); }
. vital-card . temp :: before { background : linear-gradient ( 180 deg , var ( -- temp - color ), transparent ); }
. vital-card . map-card :: before { background : linear-gradient ( 180 deg , var ( -- map - color ), transparent ); }
2026-07-08 14:14:03 +05:30
/* Glow effect behind cards */
. vital-card :: after {
content : '' ;
position : absolute ;
top : 50 % ;
2026-07-13 16:35:33 +05:30
left : 20 % ;
2026-07-13 16:42:16 +05:30
width : 80 px ;
height : 80 px ;
2026-07-08 14:14:03 +05:30
border-radius : 50 % ;
transform : translate ( -50 % , -50 % );
opacity : 0 ;
transition : opacity 0.6 s ;
pointer-events : none ;
z-index : 0 ;
}
. vital-card . hr :: after { background : radial-gradient ( circle , var ( -- hr - glow ), transparent 70 % ); }
. vital-card . spo2 :: after { background : radial-gradient ( circle , var ( -- spo2 - glow ), transparent 70 % ); }
. vital-card . bp :: after { background : radial-gradient ( circle , var ( -- bp - glow ), transparent 70 % ); }
. vital-card . rr :: after { background : radial-gradient ( circle , var ( -- rr - glow ), transparent 70 % ); }
. vital-card . temp :: after { background : radial-gradient ( circle , var ( -- temp - glow ), transparent 70 % ); }
. vital-card . map-card :: after { background : radial-gradient ( circle , var ( -- map - glow ), transparent 70 % ); }
. vital-card . has-value :: after {
2026-07-13 16:44:54 +05:30
opacity : 0.15 ;
2026-07-08 14:14:03 +05:30
}
. vital-card . has-value : hover :: after {
2026-07-13 16:44:54 +05:30
opacity : 0.3 ;
2026-07-08 14:14:03 +05:30
}
. card-header {
2026-07-13 16:35:33 +05:30
grid-area : header ;
2026-07-08 14:14:03 +05:30
display : flex ;
2026-07-13 16:44:54 +05:30
flex-direction : column ;
align-items : flex-start ;
justify-content : center ;
2026-07-08 14:14:03 +05:30
position : relative ;
z-index : 1 ;
2026-07-13 16:44:54 +05:30
padding-bottom : 0 ;
margin-bottom : 0 ;
2026-07-08 14:14:03 +05:30
}
. card-label {
display : flex ;
align-items : center ;
2026-07-13 16:44:54 +05:30
gap : 4 px ;
2026-07-08 14:14:03 +05:30
}
. card-icon {
2026-07-13 16:44:54 +05:30
display : none !important ;
2026-07-08 14:14:03 +05:30
}
. card-name {
2026-07-13 16:44:54 +05:30
font-size : 10 px ;
2026-07-13 15:17:30 +05:30
font-weight : 700 ;
2026-07-08 14:14:03 +05:30
text-transform : uppercase ;
2026-07-13 16:44:54 +05:30
letter-spacing : 0.3 px ;
2026-07-08 14:14:03 +05:30
}
. vital-card . hr . card-name { color : var ( -- hr - color ); }
. vital-card . spo2 . card-name { color : var ( -- spo2 - color ); }
. vital-card . bp . card-name { color : var ( -- bp - color ); }
. vital-card . rr . card-name { color : var ( -- rr - color ); }
. vital-card . temp . card-name { color : var ( -- temp - color ); }
. vital-card . map-card . card-name { color : var ( -- map - color ); }
. status-badge {
2026-07-13 16:44:54 +05:30
display : none !important ;
2026-07-08 14:14:03 +05:30
}
/* === Vital Value Display === */
. vital-value-container {
2026-07-13 16:35:33 +05:30
grid-area : value ;
2026-07-08 14:14:03 +05:30
display : flex ;
align-items : center ;
2026-07-13 16:35:33 +05:30
justify-content : flex-start ;
2026-07-08 14:14:03 +05:30
position : relative ;
z-index : 1 ;
2026-07-13 16:35:33 +05:30
height : 100 % ;
2026-07-08 14:14:03 +05:30
}
. vital-value {
font-family : 'JetBrains Mono' , monospace ;
2026-07-13 16:52:41 +05:30
font-size : 28 px ;
2026-07-08 14:14:03 +05:30
font-weight : 700 ;
line-height : 1 ;
2026-07-13 16:44:54 +05:30
letter-spacing : -0.5 px ;
2026-07-08 14:14:03 +05:30
transition : all 0.5 s cubic-bezier ( 0.16 , 1 , 0.3 , 1 );
}
2026-07-13 16:42:16 +05:30
. vital-card . hr . vital-value { color : var ( -- hr - color ); text-shadow : 0 0 4 px rgba ( 220 , 38 , 38 , 0.12 ); }
. vital-card . spo2 . vital-value { color : var ( -- spo2 - color ); text-shadow : 0 0 4 px rgba ( 8 , 145 , 178 , 0.12 ); }
. vital-card . bp . vital-value { color : var ( -- bp - color ); text-shadow : 0 0 4 px rgba ( 124 , 58 , 237 , 0.12 ); }
. vital-card . rr . vital-value { color : var ( -- rr - color ); text-shadow : 0 0 4 px rgba ( 22 , 163 , 74 , 0.12 ); }
. vital-card . temp . vital-value { color : var ( -- temp - color ); text-shadow : 0 0 4 px rgba ( 234 , 88 , 12 , 0.12 ); }
. vital-card . map-card . vital-value { color : var ( -- map - color ); text-shadow : 0 0 4 px rgba ( 219 , 39 , 119 , 0.12 ); }
2026-07-08 14:14:03 +05:30
. vital-value . no-data {
2026-07-13 16:52:41 +05:30
font-size : 22 px ;
2026-07-08 14:14:03 +05:30
color : var ( -- text - dim ) !important ;
2026-07-13 16:35:33 +05:30
text-shadow : none !important ;
2026-07-08 14:14:03 +05:30
}
. vital-value . unit {
2026-07-13 16:52:41 +05:30
font-size : 11 px ;
2026-07-13 15:17:30 +05:30
font-weight : 500 ;
2026-07-08 14:14:03 +05:30
letter-spacing : 0 ;
2026-07-13 16:24:37 +05:30
margin-left : 2 px ;
2026-07-13 15:17:30 +05:30
color : var ( -- text - secondary );
2026-07-13 16:35:33 +05:30
text-shadow : none !important ;
2026-07-08 14:14:03 +05:30
}
. vital-value . slash {
2026-07-13 16:52:41 +05:30
font-size : 22 px ;
2026-07-13 15:17:30 +05:30
opacity : 0.3 ;
2026-07-13 16:24:37 +05:30
margin : 0 ;
2026-07-13 15:17:30 +05:30
color : var ( -- text - secondary );
2026-07-13 16:35:33 +05:30
text-shadow : none !important ;
2026-07-08 14:14:03 +05:30
}
. vital-value-update {
animation : valueFlash 0.6 s ease-out ;
}
@ keyframes valueFlash {
2026-07-13 16:44:54 +05:30
0 % { transform : scale ( 1.02 ); filter : brightness ( 1.08 ); }
2026-07-08 14:14:03 +05:30
100 % { transform : scale ( 1 ); filter : brightness ( 1 ); }
}
/* === Waveform Canvas === */
. waveform-container {
2026-07-13 16:35:33 +05:30
grid-area : waveform ;
2026-07-13 16:52:41 +05:30
height : 40 px ;
2026-07-08 14:14:03 +05:30
position : relative ;
z-index : 1 ;
overflow : hidden ;
2026-07-13 16:42:16 +05:30
border-radius : 4 px ;
2026-07-13 15:17:30 +05:30
background : rgba ( 0 , 0 , 0 , 0.01 );
2026-07-08 14:14:03 +05:30
}
. waveform-container canvas {
width : 100 % ;
height : 100 % ;
display : block ;
}
2026-07-13 16:44:54 +05:30
/* === Normal Range Footer === */
2026-07-08 14:14:03 +05:30
. card-footer {
2026-07-13 16:42:16 +05:30
display : none !important ;
2026-07-08 14:14:03 +05:30
}
/* === Info Bar === */
. info-bar {
2026-07-13 16:24:37 +05:30
display : none !important ;
2026-07-08 14:14:03 +05:30
}
@ media ( max-width : 1024px ) {
. vitals-grid {
grid-template-columns : repeat ( 2 , 1 fr );
2026-07-13 16:35:33 +05:30
gap : 12 px ;
2026-07-08 14:14:03 +05:30
}
. setup-guide-body {
grid-template-columns : 1 fr ;
}
}
@ media ( max-width : 640px ) {
. app-container { padding : 12 px ; }
. vitals-grid {
grid-template-columns : 1 fr ;
2026-07-13 16:35:33 +05:30
gap : 12 px ;
2026-07-08 14:14:03 +05:30
}
. header {
flex-direction : column ;
2026-07-13 15:17:30 +05:30
gap : 16 px ;
2026-07-08 14:14:03 +05:30
text-align : center ;
2026-07-13 15:17:30 +05:30
align-items : center ;
padding : 20 px ;
}
. header-left {
flex-direction : column ;
align-items : center ;
gap : 10 px ;
2026-07-08 14:14:03 +05:30
}
. header-right {
2026-07-13 15:17:30 +05:30
flex-direction : column ;
width : 100 % ;
gap : 12 px ;
2026-07-08 14:14:03 +05:30
}
2026-07-13 15:17:30 +05:30
. device-selector-container {
width : 100 % ;
justify-content : center ;
margin-right : 0 ;
}
. device-select-dropdown {
width : 100 % ;
max-width : 250 px ;
}
2026-07-08 14:14:03 +05:30
. setup-guide-body {
grid-template-columns : 1 fr ;
}
}
2026-07-13 15:23:10 +05:30
/* === Patient Clinical Info Banner === */
. patient-banner {
display : flex ;
align-items : center ;
2026-07-13 16:24:37 +05:30
justify-content : flex-start ;
gap : 20 px ;
padding : 8 px 16 px ;
2026-07-13 15:23:10 +05:30
background : var ( -- bg - card );
border : 1 px solid var ( -- border - color );
2026-07-13 16:24:37 +05:30
border-radius : 12 px ;
2026-07-13 15:23:10 +05:30
box-shadow :
2026-07-13 16:24:37 +05:30
0 2 px 4 px -1 px rgba ( 0 , 0 , 0 , 0.01 ),
0 4 px 6 px -3 px rgba ( 0 , 0 , 0 , 0.02 );
margin-bottom : 12 px ;
2026-07-13 15:23:10 +05:30
flex-wrap : wrap ;
}
. patient-banner-group {
display : flex ;
align-items : center ;
2026-07-13 16:24:37 +05:30
gap : 8 px ;
2026-07-13 15:23:10 +05:30
}
. patient-avatar {
2026-07-13 16:24:37 +05:30
width : 28 px ;
height : 28 px ;
2026-07-13 15:23:10 +05:30
border-radius : 50 % ;
2026-07-13 16:24:37 +05:30
background : rgba ( 8 , 145 , 178 , 0.06 );
2026-07-13 15:23:10 +05:30
color : var ( -- spo2 - color );
display : flex ;
align-items : center ;
justify-content : center ;
}
. patient-avatar svg {
2026-07-13 16:24:37 +05:30
width : 14 px ;
height : 14 px ;
2026-07-13 15:23:10 +05:30
}
. patient-label {
2026-07-13 16:24:37 +05:30
font-size : 9 px ;
2026-07-13 15:23:10 +05:30
color : var ( -- text - secondary );
font-weight : 700 ;
text-transform : uppercase ;
2026-07-13 16:24:37 +05:30
letter-spacing : 0.3 px ;
2026-07-13 15:23:10 +05:30
}
. patient-value-text {
2026-07-13 16:24:37 +05:30
font-size : 12 px ;
2026-07-13 15:23:10 +05:30
color : var ( -- text - primary );
font-weight : 700 ;
}
. patient-banner-divider {
2026-07-13 16:24:37 +05:30
height : 16 px ;
2026-07-13 15:23:10 +05:30
width : 1 px ;
background : var ( -- border - color );
}
. vital-svg {
stroke-width : 2.5 ;
transition : transform 0.3 s ease ;
}
. vital-card : hover . vital-svg {
transform : scale ( 1.1 );
}
. vital-card . hr . card-icon { color : var ( -- hr - color ); }
. vital-card . spo2 . card-icon { color : var ( -- spo2 - color ); }
. vital-card . bp . card-icon { color : var ( -- bp - color ); }
. vital-card . rr . card-icon { color : var ( -- rr - color ); }
. vital-card . temp . card-icon { color : var ( -- temp - color ); }
. vital-card . map-card . card-icon { color : var ( -- map - color ); }
@ media ( max-width : 768px ) {
. patient-banner-divider {
display : none ;
}
. patient-banner {
display : grid ;
grid-template-columns : repeat ( 2 , 1 fr );
gap : 16 px ;
}
}
@ media ( max-width : 480px ) {
. patient-banner {
grid-template-columns : 1 fr ;
}
}
2026-07-13 15:27:21 +05:30
/* === Minimal mode for clean iframe embeds === */
body . minimal-mode . header { display : none !important ; }
body . minimal-mode . patient-banner { display : none !important ; }
body . minimal-mode . setup-guide { display : none !important ; }
body . minimal-mode . info-bar { display : none !important ; }
body . minimal-mode . app-container {
padding : 0 !important ;
max-width : 100 % !important ;
margin : 0 !important ;
}
body . minimal-mode {
padding : 0 !important ;
background : transparent !important ;
}
body . minimal-mode . bg-grid , body . minimal-mode . bg-glow {
display : none !important ;
}
2026-07-15 11:33:48 +05:30
/* === Verification Failure Overlay === */
. verification-overlay {
position : fixed ;
inset : 0 ;
background : rgba ( 248 , 250 , 252 , 0.98 );
z-index : 9999 ;
display : flex ;
align-items : center ;
justify-content : center ;
flex-direction : column ;
text-align : center ;
padding : 24 px ;
color : var ( -- text - primary );
}
. verification-overlay . error-card {
background : #ffffff ;
border : 1 px solid rgba ( 220 , 38 , 38 , 0.12 );
box-shadow : 0 10 px 30 px rgba ( 0 , 0 , 0 , 0.05 );
border-radius : 16 px ;
padding : 32 px ;
max-width : 460 px ;
width : 100 % ;
transition : all 0.3 s ease ;
}
. verification-overlay . error-icon {
font-size : 40 px ;
margin-bottom : 16 px ;
}
. verification-overlay h2 {
font-size : 19 px ;
font-weight : 700 ;
margin-bottom : 8 px ;
color : #dc2626 ;
}
. verification-overlay p {
font-size : 13 px ;
color : var ( -- text - secondary );
line-height : 1.6 ;
}
. verification-overlay . loading h2 {
color : var ( -- spo2 - color );
}
. verification-overlay . loading . error-icon {
animation : rotateGlow 2 s linear infinite ;
}
@ keyframes rotateGlow {
0 % { transform : rotate ( 0 deg ); }
100 % { transform : rotate ( 360 deg ); }
}
2026-07-08 14:14:03 +05:30
</ style >
</ head >
< body >
2026-07-15 11:33:48 +05:30
<!-- Patient Verification Overlay -->
< div id = "verificationOverlay" class = "verification-overlay" style = "display: none;" >
< div class = "error-card" id = "verificationCard" >
< div class = "error-icon" id = "verificationIcon" > 🔒</ div >
< h2 id = "verificationTitle" > Patient Verification Required</ h2 >
< p id = "verificationMsg" > Telemetry data is only displayed once the patient's MRN, Name, Gender, and DOB are fully verified against the active monitor.</ p >
</ div >
</ div >
2026-07-08 14:14:03 +05:30
< div class = "bg-grid" ></ div >
< div class = "bg-glow" ></ div >
< div class = "app-container" >
<!-- Header -->
< header class = "header" id = "header" >
< div class = "header-left" >
< div >
< div class = "header-title" > Patient Vital Signs Monitor</ div >
2026-07-11 14:24:14 +05:30
< div class = "header-subtitle" > Contec Monitor — Real-time Monitoring</ div >
2026-07-08 14:14:03 +05:30
</ div >
</ div >
< div class = "header-right" >
2026-07-11 14:24:14 +05:30
< div class = "device-selector-container" >
< label for = "deviceSelector" style = "font-size: 13px; color: var(--text-secondary); font-weight: 500;" > Select Monitor:</ label >
< select id = "deviceSelector" class = "device-select-dropdown" onchange = "selectDevice(this.value)" >
< option value = "" > Waiting for devices...</ option >
</ select >
</ div >
2026-07-08 14:14:03 +05:30
< div class = "audio-toggle" id = "audioToggle" onclick = "toggleAudioAlert()" title = "Toggle audio alerts for critical vitals" >
< span id = "audioIcon" > 🔇</ span >
< span id = "audioText" > Alerts Off</ span >
</ div >
< div class = "connection-badge offline" id = "connectionBadge" >
< div class = "status-dot" ></ div >
< span id = "connectionText" > Connecting...</ span >
</ div >
< div class = "time-display" id = "timeDisplay" > --:--:--</ div >
</ div >
</ header >
<!-- Vitals Grid -->
< div class = "vitals-grid" >
<!-- Heart Rate -->
< div class = "vital-card hr" id = "card-hr" >
< div class = "card-header" >
< div class = "card-label" >
2026-07-13 15:23:10 +05:30
< div class = "card-icon" >
< svg class = "vital-svg" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-linecap = "round" stroke-linejoin = "round" >
< path d = "M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z" />
</ svg >
</ div >
2026-07-08 14:14:03 +05:30
< span class = "card-name" > Heart Rate</ span >
</ div >
< span class = "status-badge offline" id = "status-hr" > No Data</ span >
</ div >
< div class = "vital-value-container" >
< div class = "vital-value no-data" id = "value-hr" > ---</ div >
</ div >
< div class = "waveform-container" >
< canvas id = "waveform-hr" ></ canvas >
</ div >
< div class = "card-footer" >
< span class = "normal-range" > Normal: 60– 100 bpm</ span >
< span class = "last-update" id = "update-hr" > --</ span >
</ div >
</ div >
<!-- SpO2 -->
< div class = "vital-card spo2" id = "card-spo2" >
< div class = "card-header" >
< div class = "card-label" >
2026-07-13 15:23:10 +05:30
< div class = "card-icon" >
< svg class = "vital-svg" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-linecap = "round" stroke-linejoin = "round" >
< path d = "M7 3a3 3 0 0 0-3 3v6a3 3 0 0 0 3 3h1a2 2 0 0 0 2-2V6a3 3 0 0 0-3-3z" />
< path d = "M17 3a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3h-1a2 2 0 0 1-2-2V6a3 3 0 0 1 3-3z" />
< path d = "M9 20h6" />
< path d = "M12 16v4" />
</ svg >
</ div >
2026-07-08 14:14:03 +05:30
< span class = "card-name" > SpO₂</ span >
</ div >
< span class = "status-badge offline" id = "status-spo2" > No Data</ span >
</ div >
< div class = "vital-value-container" >
< div class = "vital-value no-data" id = "value-spo2" > ---</ div >
</ div >
< div class = "waveform-container" >
< canvas id = "waveform-spo2" ></ canvas >
</ div >
< div class = "card-footer" >
< span class = "normal-range" > Normal: 95– 100%</ span >
< span class = "last-update" id = "update-spo2" > --</ span >
</ div >
</ div >
<!-- Blood Pressure -->
< div class = "vital-card bp" id = "card-bp" >
< div class = "card-header" >
< div class = "card-label" >
2026-07-13 15:23:10 +05:30
< div class = "card-icon" >
< svg class = "vital-svg" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-linecap = "round" stroke-linejoin = "round" >
< path d = "M12 22a7 7 0 0 0 7-7c0-4.3-7-13-7-13S5 10.7 5 15a7 7 0 0 0 7 7z" />
</ svg >
</ div >
2026-07-08 14:14:03 +05:30
< span class = "card-name" > Blood Pressure</ span >
</ div >
< span class = "status-badge offline" id = "status-bp" > No Data</ span >
</ div >
< div class = "vital-value-container" >
< div class = "vital-value no-data" id = "value-bp" > ---</ div >
</ div >
< div class = "waveform-container" >
< canvas id = "waveform-bp" ></ canvas >
</ div >
< div class = "card-footer" >
< span class = "normal-range" > Normal: 90/60– 140/90 mmHg</ span >
< span class = "last-update" id = "update-bp" > --</ span >
</ div >
</ div >
<!-- Respiratory Rate -->
< div class = "vital-card rr" id = "card-rr" >
< div class = "card-header" >
< div class = "card-label" >
2026-07-13 15:23:10 +05:30
< div class = "card-icon" >
< svg class = "vital-svg" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-linecap = "round" stroke-linejoin = "round" >
< path d = "M2 8h16a4 4 0 0 1 4 4v0a4 4 0 0 1-4 4H2" />
< path d = "M6 12h12a2 2 0 0 0 2-2v0a2 2 0 0 0-2-2H6" />
< path d = "M2 16h8" />
</ svg >
</ div >
2026-07-08 14:14:03 +05:30
< span class = "card-name" > Respiratory Rate</ span >
</ div >
< span class = "status-badge offline" id = "status-rr" > No Data</ span >
</ div >
< div class = "vital-value-container" >
< div class = "vital-value no-data" id = "value-rr" > ---</ div >
</ div >
< div class = "waveform-container" >
< canvas id = "waveform-rr" ></ canvas >
</ div >
< div class = "card-footer" >
< span class = "normal-range" > Normal: 12– 20 /min</ span >
< span class = "last-update" id = "update-rr" > --</ span >
</ div >
</ div >
<!-- Temperature -->
< div class = "vital-card temp" id = "card-temp" >
< div class = "card-header" >
< div class = "card-label" >
2026-07-13 15:23:10 +05:30
< div class = "card-icon" >
< svg class = "vital-svg" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-linecap = "round" stroke-linejoin = "round" >
< path d = "M14 14.76V3.5a2.5 2.5 0 0 0-5 0v11.26a4.5 4.5 0 1 0 5 0z" />
< line x1 = "11.5" y1 = "5" x2 = "11.5" y2 = "13" />
</ svg >
</ div >
2026-07-08 14:14:03 +05:30
< span class = "card-name" > Temperature</ span >
</ div >
< span class = "status-badge offline" id = "status-temp" > No Data</ span >
</ div >
< div class = "vital-value-container" >
< div class = "vital-value no-data" id = "value-temp" > ---</ div >
</ div >
< div class = "waveform-container" >
< canvas id = "waveform-temp" ></ canvas >
</ div >
< div class = "card-footer" >
2026-07-13 16:05:17 +05:30
< span class = "normal-range" > Normal: 97.0– 99.0 °F</ span >
2026-07-08 14:14:03 +05:30
< span class = "last-update" id = "update-temp" > --</ span >
</ div >
</ div >
<!-- MAP -->
< div class = "vital-card map-card" id = "card-map" >
< div class = "card-header" >
< div class = "card-label" >
2026-07-13 15:23:10 +05:30
< div class = "card-icon" >
< svg class = "vital-svg" viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-linecap = "round" stroke-linejoin = "round" >
< polyline points = "22 12 18 12 15 21 9 3 6 12 2 12" />
</ svg >
</ div >
2026-07-08 14:14:03 +05:30
< span class = "card-name" > Mean Arterial Pressure</ span >
</ div >
< span class = "status-badge offline" id = "status-map" > No Data</ span >
</ div >
< div class = "vital-value-container" >
< div class = "vital-value no-data" id = "value-map" > ---</ div >
</ div >
< div class = "waveform-container" >
< canvas id = "waveform-map" ></ canvas >
</ div >
< div class = "card-footer" >
< span class = "normal-range" > Normal: 70– 105 mmHg</ span >
< span class = "last-update" id = "update-map" > --</ span >
</ div >
</ div >
</ div >
<!-- Info Bar -->
< div class = "info-bar" >
< div class = "info-item" >
< span > Model:</ span >
< span class = "model-badge" id = "modelBadge" > CMS7000PLUS</ span >
</ div >
< div class = "info-dot" ></ div >
< div class = "info-item" >
< span > Device:</ span >
< strong id = "deviceId" > Waiting...</ strong >
</ div >
< div class = "info-dot" ></ div >
< div class = "info-item" >
< span > Patient:</ span >
< strong id = "patientId" > Waiting...</ strong >
</ div >
< div class = "info-dot" ></ div >
< div class = "info-item" >
< span > Readings:</ span >
< strong id = "totalReadings" > 0</ strong >
</ div >
< div class = "info-dot" ></ div >
< div class = "info-item" >
< span > Last Update:</ span >
< strong id = "lastUpdateTime" > Never</ strong >
</ div >
< div class = "info-dot" ></ div >
< div class = "info-item" >
< span > API:</ span >
< strong id = "apiStatus" > Checking...</ strong >
</ div >
</ div >
</ div >
< script >
// === Configuration ===
const API_BASE = window . location . origin + '/api' ;
const POLL_INTERVAL = 2000 ; // 2 seconds
const WS_URL = `ws:// ${ window . location . host } /ws/vitals` ;
// === Vital ranges for status calculation ===
const VITAL_RANGES = {
heart_rate : { low : 60 , high : 100 , critLow : 40 , critHigh : 150 },
spo2 : { low : 95 , high : 100 , critLow : 90 , critHigh : 101 },
systolic_bp : { low : 90 , high : 140 , critLow : 70 , critHigh : 180 },
diastolic_bp : { low : 60 , high : 90 , critLow : 40 , critHigh : 120 },
map_bp : { low : 70 , high : 105 , critLow : 60 , critHigh : 130 },
respiratory_rate : { low : 12 , high : 20 , critLow : 8 , critHigh : 30 },
2026-07-13 16:05:17 +05:30
temperature : { low : 97.0 , high : 99.0 , critLow : 95.0 , critHigh : 102.2 },
2026-07-08 14:14:03 +05:30
};
// === State ===
let lastReadingId = null ;
let readingCount = 0 ;
let waveformData = {
hr : [], spo2 : [], bp : [], rr : [], temp : [], map : []
};
let waveformCanvases = {};
let useWebSocket = false ;
let ws = null ;
let audioAlertEnabled = false ;
let audioContext = null ;
let setupGuideCollapsed = false ;
let hasReceivedData = false ;
2026-07-11 14:24:14 +05:30
let activeDevices = [];
let selectedDeviceId = localStorage . getItem ( 'selectedDeviceId' ) || '' ;
2026-07-15 11:33:48 +05:30
// Verification / Matching Mode State
let isMatchingMode = false ;
let matchParams = {};
async function verifyPatientMatch () {
const overlay = document . getElementById ( 'verificationOverlay' );
const icon = document . getElementById ( 'verificationIcon' );
const title = document . getElementById ( 'verificationTitle' );
const msg = document . getElementById ( 'verificationMsg' );
try {
const query = new URLSearchParams ( matchParams ). toString ();
const response = await fetch ( ` ${ API_BASE } /match-patient? ${ query } ` );
if ( ! response . ok ) throw new Error ( 'Match verification API error' );
const data = await response . json ();
if ( data . matched ) {
if ( data . device_id ) {
// Success - matched and connected!
overlay . style . display = 'none' ;
// Hide device selector element to restrict access
const selectorContainer = document . querySelector ( '.device-selector-container' );
if ( selectorContainer ) selectorContainer . style . display = 'none' ;
if ( selectedDeviceId !== data . device_id ) {
selectedDeviceId = data . device_id ;
clearVitalsDisplay ();
}
} else {
// Matched but device not transmitting yet
overlay . style . display = 'flex' ;
overlay . className = 'verification-overlay loading' ;
icon . textContent = '⏳' ;
title . textContent = 'Connecting Monitor...' ;
msg . textContent = `Patient ${ data . patient_name || 'Record' } verified. Waiting for monitor to start transmitting...` ;
selectedDeviceId = '' ;
clearVitalsDisplay ();
}
} else {
// Match failed
overlay . style . display = 'flex' ;
overlay . className = 'verification-overlay' ;
icon . textContent = '❌' ;
title . textContent = 'Access Restricted' ;
msg . textContent = data . reason || 'This patient record does not match the active monitor configuration.' ;
selectedDeviceId = '' ;
clearVitalsDisplay ();
}
} catch ( e ) {
console . error ( "Match verification failed:" , e );
// On API error, show connecting/retry state
overlay . style . display = 'flex' ;
overlay . className = 'verification-overlay loading' ;
icon . textContent = '🔄' ;
title . textContent = 'Verification Server Offline' ;
msg . textContent = 'Retrying verification match...' ;
}
}
2026-07-08 14:14:03 +05:30
// === Initialize ===
document . addEventListener ( 'DOMContentLoaded' , () => {
2026-07-13 15:27:21 +05:30
const urlParams = new URLSearchParams ( window . location . search );
const isMinimal = urlParams . get ( 'minimal' ) === 'true' || urlParams . get ( 'clean' ) === 'true' ;
if ( isMinimal ) {
document . body . classList . add ( 'minimal-mode' );
}
2026-07-15 11:33:48 +05:30
// Extract matching credentials from URL query params
const mrn = urlParams . get ( 'mrn' );
const gender = urlParams . get ( 'gender' );
const dob = urlParams . get ( 'dob' );
const age = urlParams . get ( 'age' );
2026-07-15 11:58:20 +05:30
if ( mrn ) {
2026-07-15 11:33:48 +05:30
isMatchingMode = true ;
2026-07-15 11:58:20 +05:30
matchParams = { mrn , gender , dob , age };
2026-07-15 11:33:48 +05:30
// Show verification loading overlay initially
const overlay = document . getElementById ( 'verificationOverlay' );
overlay . style . display = 'flex' ;
overlay . className = 'verification-overlay loading' ;
document . getElementById ( 'verificationIcon' ). textContent = '🔄' ;
document . getElementById ( 'verificationTitle' ). textContent = 'Verifying Patient Credentials...' ;
2026-07-15 11:58:20 +05:30
document . getElementById ( 'verificationMsg' ). textContent = 'Matching MRN, Gender, DOB, and Age with live patient monitors...' ;
2026-07-15 11:33:48 +05:30
}
2026-07-08 14:14:03 +05:30
initWaveforms ();
updateClock ();
setInterval ( updateClock , 1000 );
2026-07-15 11:33:48 +05:30
if ( isMatchingMode ) {
// In matching mode, verify credentials first, then poll/ws
verifyPatientMatch (). then (() => {
tryWebSocket ();
startPolling ();
});
// Periodically verify the match and update device mapping
setInterval ( verifyPatientMatch , 5000 );
} else {
fetchDevices (). then (() => {
tryWebSocket ();
startPolling ();
});
// Periodically refresh the list of active devices
setInterval ( fetchDevices , 4000 );
}
2026-07-08 14:14:03 +05:30
animateWaveforms ();
fetchNetworkInfo ();
});
// === Fetch Network Info ===
async function fetchNetworkInfo () {
try {
const response = await fetch ( ` ${ API_BASE } /network-info` );
if ( response . ok ) {
const info = await response . json ();
const ipEl = document . getElementById ( 'detectedIp' );
if ( info . primary_ip ) {
ipEl . textContent = info . primary_ip ;
} else if ( info . ip_addresses && info . ip_addresses . length > 0 ) {
ipEl . textContent = info . ip_addresses . join ( ' / ' );
} else {
ipEl . textContent = 'Run ipconfig to find your IP' ;
}
// Update model badge
if ( info . monitor_model ) {
document . getElementById ( 'modelBadge' ). textContent = info . monitor_model ;
}
}
} catch ( e ) {
document . getElementById ( 'detectedIp' ). textContent = 'Check ipconfig' ;
}
}
2026-07-11 14:24:14 +05:30
// === Fetch Devices List ===
async function fetchDevices () {
try {
const response = await fetch ( ` ${ API_BASE } /devices` );
if ( response . ok ) {
const devices = await response . json ();
activeDevices = devices ;
updateDeviceSelector ();
}
} catch ( e ) {
console . error ( "Error fetching devices:" , e );
}
}
// === Update Device Dropdown Selector ===
function updateDeviceSelector () {
const selector = document . getElementById ( 'deviceSelector' );
const previousSelection = selector . value || selectedDeviceId ;
selector . innerHTML = '' ;
if ( activeDevices . length === 0 ) {
const opt = document . createElement ( 'option' );
opt . value = '' ;
opt . textContent = 'No monitors connected' ;
selector . appendChild ( opt );
return ;
}
// Sort: active devices first, then offline
const sortedDevices = [... activeDevices ]. sort (( a , b ) => {
if ( a . status === 'active' && b . status !== 'active' ) return - 1 ;
if ( a . status !== 'active' && b . status === 'active' ) return 1 ;
return new Date ( b . last_seen ) - new Date ( a . last_seen );
});
sortedDevices . forEach ( d => {
const opt = document . createElement ( 'option' );
opt . value = d . device_id ;
const statusSymbol = d . status === 'active' ? '🟢' : '⚫' ;
const ipStr = d . ip_address && d . ip_address !== 'unknown' ? ` ( ${ d . ip_address } )` : '' ;
opt . textContent = ` ${ statusSymbol } ${ d . device_id }${ ipStr } ` ;
selector . appendChild ( opt );
});
if ( previousSelection && sortedDevices . some ( d => d . device_id === previousSelection )) {
selector . value = previousSelection ;
selectedDeviceId = previousSelection ;
} else {
const firstActive = sortedDevices . find ( d => d . status === 'active' );
if ( firstActive ) {
selector . value = firstActive . device_id ;
selectedDeviceId = firstActive . device_id ;
} else if ( sortedDevices . length > 0 ) {
selector . value = sortedDevices [ 0 ]. device_id ;
selectedDeviceId = sortedDevices [ 0 ]. device_id ;
}
}
localStorage . setItem ( 'selectedDeviceId' , selectedDeviceId );
}
// === Change Selected Device ===
function selectDevice ( deviceId ) {
selectedDeviceId = deviceId ;
localStorage . setItem ( 'selectedDeviceId' , deviceId );
clearVitalsDisplay ();
fetchLatestReadings ();
}
// === Clear Vitals from Screen ===
function clearVitalsDisplay () {
[ 'hr' , 'spo2' , 'bp' , 'rr' , 'temp' , 'map' ]. forEach ( key => {
const valueEl = document . getElementById ( `value- ${ key } ` );
const statusEl = document . getElementById ( `status- ${ key } ` );
const cardEl = document . getElementById ( `card- ${ key } ` );
valueEl . innerHTML = '---' ;
valueEl . className = 'vital-value no-data' ;
statusEl . textContent = 'No Data' ;
statusEl . className = 'status-badge offline' ;
cardEl . classList . remove ( 'has-value' );
waveformData [ key ] = [];
});
document . getElementById ( 'deviceId' ). textContent = 'Waiting...' ;
document . getElementById ( 'patientId' ). textContent = 'Waiting...' ;
2026-07-13 15:23:10 +05:30
document . getElementById ( 'bannerPatientId' ). textContent = 'N/A' ;
document . getElementById ( 'bannerPatientName' ). textContent = 'John Doe' ;
document . getElementById ( 'bannerMonitorModel' ). textContent = 'CMS8500' ;
document . getElementById ( 'bannerLastSeen' ). textContent = 'Never' ;
2026-07-11 14:24:14 +05:30
lastReadingId = null ;
}
2026-07-08 14:14:03 +05:30
// === Setup Guide Toggle ===
function toggleSetupGuide () {
const body = document . getElementById ( 'setupGuideBody' );
const toggleText = document . getElementById ( 'setupToggleText' );
setupGuideCollapsed = ! setupGuideCollapsed ;
if ( setupGuideCollapsed ) {
body . style . display = 'none' ;
toggleText . textContent = '▸ Expand' ;
} else {
body . style . display = 'grid' ;
toggleText . textContent = '▾ Collapse' ;
}
}
// === Audio Alert Toggle ===
function toggleAudioAlert () {
audioAlertEnabled = ! audioAlertEnabled ;
const toggle = document . getElementById ( 'audioToggle' );
const icon = document . getElementById ( 'audioIcon' );
const text = document . getElementById ( 'audioText' );
if ( audioAlertEnabled ) {
toggle . classList . add ( 'active' );
icon . textContent = '🔊' ;
text . textContent = 'Alerts On' ;
// Initialize audio context on user gesture
if ( ! audioContext ) {
audioContext = new ( window . AudioContext || window . webkitAudioContext )();
}
} else {
toggle . classList . remove ( 'active' );
icon . textContent = '🔇' ;
text . textContent = 'Alerts Off' ;
}
}
function playAlertBeep () {
if ( ! audioAlertEnabled || ! audioContext ) return ;
try {
const oscillator = audioContext . createOscillator ();
const gainNode = audioContext . createGain ();
oscillator . connect ( gainNode );
gainNode . connect ( audioContext . destination );
oscillator . type = 'sine' ;
oscillator . frequency . setValueAtTime ( 880 , audioContext . currentTime );
gainNode . gain . setValueAtTime ( 0.3 , audioContext . currentTime );
gainNode . gain . exponentialRampToValueAtTime ( 0.01 , audioContext . currentTime + 0.5 );
oscillator . start ( audioContext . currentTime );
oscillator . stop ( audioContext . currentTime + 0.5 );
// Second beep
const osc2 = audioContext . createOscillator ();
const gain2 = audioContext . createGain ();
osc2 . connect ( gain2 );
gain2 . connect ( audioContext . destination );
osc2 . type = 'sine' ;
osc2 . frequency . setValueAtTime ( 1100 , audioContext . currentTime + 0.6 );
gain2 . gain . setValueAtTime ( 0.3 , audioContext . currentTime + 0.6 );
gain2 . gain . exponentialRampToValueAtTime ( 0.01 , audioContext . currentTime + 1.1 );
osc2 . start ( audioContext . currentTime + 0.6 );
osc2 . stop ( audioContext . currentTime + 1.1 );
} catch ( e ) {
// Audio not supported
}
}
// === Clock ===
function updateClock () {
const now = new Date ();
document . getElementById ( 'timeDisplay' ). textContent =
now . toLocaleTimeString ( 'en-US' , { hour12 : false });
}
// === WebSocket Connection ===
function tryWebSocket () {
try {
ws = new WebSocket ( WS_URL );
ws . onopen = () => {
useWebSocket = true ;
updateConnectionStatus ( 'connected' , 'Live — WebSocket' );
};
ws . onmessage = ( event ) => {
const data = JSON . parse ( event . data );
2026-07-11 14:24:14 +05:30
// Filter messages by selected device or auto-select if nothing selected yet
2026-07-15 11:33:48 +05:30
if ( ! selectedDeviceId && data . device_id && ! isMatchingMode ) {
2026-07-11 14:24:14 +05:30
selectDevice ( data . device_id );
}
2026-07-15 11:33:48 +05:30
if ( data . device_id && data . device_id === selectedDeviceId ) {
2026-07-11 14:24:14 +05:30
updateVitals ( data );
}
// Refresh device list status
2026-07-15 11:33:48 +05:30
if ( ! isMatchingMode ) {
fetchDevices ();
}
2026-07-08 14:14:03 +05:30
};
ws . onclose = () => {
useWebSocket = false ;
updateConnectionStatus ( 'polling' , 'Polling Mode' );
setTimeout ( tryWebSocket , 5000 );
};
ws . onerror = () => {
useWebSocket = false ;
};
} catch ( e ) {
useWebSocket = false ;
}
}
// === Polling Fallback ===
function startPolling () {
fetchLatestReadings ();
setInterval ( fetchLatestReadings , POLL_INTERVAL );
}
async function fetchLatestReadings () {
2026-07-15 11:33:48 +05:30
if ( isMatchingMode && ! selectedDeviceId ) {
return ;
}
2026-07-08 14:14:03 +05:30
try {
2026-07-11 14:24:14 +05:30
const url = selectedDeviceId
? ` ${ API_BASE } /latest-readings?device_id= ${ encodeURIComponent ( selectedDeviceId ) } &limit=1`
: ` ${ API_BASE } /latest-readings?limit=1` ;
const response = await fetch ( url );
2026-07-08 14:14:03 +05:30
if ( ! response . ok ) throw new Error ( 'API error' );
const readings = await response . json ();
if ( readings . length > 0 ) {
const latest = readings [ 0 ];
if ( latest . id !== lastReadingId ) {
lastReadingId = latest . id ;
readingCount ++ ;
updateVitals ( latest );
}
if ( ! useWebSocket ) {
updateConnectionStatus ( 'connected' , 'Connected — Polling' );
}
} else {
if ( ! useWebSocket ) {
updateConnectionStatus ( 'waiting' , 'Waiting for data...' );
}
}
// Fetch system status
2026-07-15 11:33:48 +05:30
if ( ! isMatchingMode ) {
const statusResp = await fetch ( ` ${ API_BASE } /status` );
if ( statusResp . ok ) {
const status = await statusResp . json ();
document . getElementById ( 'totalReadings' ). textContent = status . total_readings_stored ;
document . getElementById ( 'apiStatus' ). textContent =
status . pending_transmissions > 0
? ` ${ status . pending_transmissions } pending`
: 'All sent ✓' ;
// Update active ports list in guide
const portsEl = document . getElementById ( 'activePortsList' );
if ( portsEl && status . active_ports && status . active_ports . length > 0 ) {
portsEl . textContent = status . active_ports . join ( ', ' );
}
2026-07-11 14:24:14 +05:30
}
2026-07-08 14:14:03 +05:30
}
} catch ( e ) {
if ( ! useWebSocket ) {
updateConnectionStatus ( 'offline' , 'Server offline' );
}
}
}
// === Update Vitals Display ===
function updateVitals ( data ) {
// Hide setup guide once data starts flowing
if ( ! hasReceivedData ) {
hasReceivedData = true ;
const guide = document . getElementById ( 'setupGuide' );
guide . classList . add ( 'hidden' );
}
updateVitalCard ( 'hr' , data . heart_rate , 'heart_rate' , v => ` ${ Math . round ( v ) } <span class="unit">bpm</span>` );
updateVitalCard ( 'spo2' , data . spo2 , 'spo2' , v => ` ${ Math . round ( v ) } <span class="unit">%</span>` );
// Blood pressure - combined display
if ( data . systolic_bp !== null && data . systolic_bp !== undefined ) {
const sys = Math . round ( data . systolic_bp );
const dia = data . diastolic_bp !== null ? Math . round ( data . diastolic_bp ) : '--' ;
updateVitalCard ( 'bp' , data . systolic_bp , 'systolic_bp' ,
() => ` ${ sys } <span class="slash">/</span> ${ dia } <span class="unit">mmHg</span>` );
} else {
updateVitalCard ( 'bp' , null , 'systolic_bp' );
}
updateVitalCard ( 'rr' , data . respiratory_rate , 'respiratory_rate' , v => ` ${ Math . round ( v ) } <span class="unit">/min</span>` );
2026-07-13 16:05:17 +05:30
const tempF = data . temperature != null ? ( data . temperature * 9 / 5 ) + 32 : null ;
updateVitalCard ( 'temp' , tempF , 'temperature' , v => ` ${ v . toFixed ( 1 ) } <span class="unit">°F</span>` );
2026-07-08 14:14:03 +05:30
updateVitalCard ( 'map' , data . map_bp , 'map_bp' , v => ` ${ Math . round ( v ) } <span class="unit">mmHg</span>` );
// Update info bar
if ( data . device_id ) document . getElementById ( 'deviceId' ). textContent = data . device_id ;
if ( data . patient_id ) document . getElementById ( 'patientId' ). textContent = data . patient_id ;
const now = new Date ();
document . getElementById ( 'lastUpdateTime' ). textContent =
now . toLocaleTimeString ( 'en-US' , { hour12 : false });
2026-07-13 15:23:10 +05:30
// Update patient clinical banner
if ( data . patient_id ) {
document . getElementById ( 'bannerPatientId' ). textContent = data . patient_id ;
document . getElementById ( 'bannerPatientName' ). textContent = data . patient_name || `Patient # ${ data . patient_id } ` ;
}
if ( data . device_id ) {
let deviceModel = 'CMS8500' ;
if ( data . device_id . includes ( 'CMS7000' )) {
deviceModel = 'CMS7000PLUS' ;
}
const parts = data . device_id . split ( '_' );
const suffix = parts . length > 1 ? ` ( ${ parts . pop () } )` : '' ;
document . getElementById ( 'bannerMonitorModel' ). textContent = ` ${ deviceModel }${ suffix } ` ;
}
document . getElementById ( 'bannerLastSeen' ). textContent = now . toLocaleTimeString ( 'en-US' , { hour12 : false });
2026-07-08 14:14:03 +05:30
// Push waveform data
pushWaveformPoint ( 'hr' , data . heart_rate );
pushWaveformPoint ( 'spo2' , data . spo2 );
pushWaveformPoint ( 'bp' , data . systolic_bp );
pushWaveformPoint ( 'rr' , data . respiratory_rate );
2026-07-13 16:05:17 +05:30
pushWaveformPoint ( 'temp' , tempF );
2026-07-08 14:14:03 +05:30
pushWaveformPoint ( 'map' , data . map_bp );
}
function updateVitalCard ( cardKey , value , rangeKey , formatter ) {
const valueEl = document . getElementById ( `value- ${ cardKey } ` );
const statusEl = document . getElementById ( `status- ${ cardKey } ` );
const updateEl = document . getElementById ( `update- ${ cardKey } ` );
const cardEl = document . getElementById ( `card- ${ cardKey } ` );
if ( value === null || value === undefined ) {
valueEl . innerHTML = '---' ;
valueEl . className = 'vital-value no-data' ;
statusEl . textContent = 'No Data' ;
statusEl . className = 'status-badge offline' ;
cardEl . classList . remove ( 'has-value' );
return ;
}
// Format value
const formatted = formatter ? formatter ( value ) : Math . round ( value );
valueEl . innerHTML = formatted ;
valueEl . className = 'vital-value vital-value-update' ;
cardEl . classList . add ( 'has-value' );
// Remove animation class after it plays
setTimeout (() => valueEl . classList . remove ( 'vital-value-update' ), 600 );
// Calculate status
const ranges = VITAL_RANGES [ rangeKey ];
let isCritical = false ;
if ( ranges ) {
if ( value <= ranges . critLow || value >= ranges . critHigh ) {
statusEl . textContent = '🚨 Critical' ;
statusEl . className = 'status-badge critical' ;
isCritical = true ;
} else if ( value < ranges . low || value > ranges . high ) {
statusEl . textContent = '⚠ Warning' ;
statusEl . className = 'status-badge warning' ;
} else {
statusEl . textContent = '✓ Normal' ;
statusEl . className = 'status-badge normal' ;
}
}
// Play alert for critical values
if ( isCritical ) {
playAlertBeep ();
}
// Update timestamp
const now = new Date ();
updateEl . textContent = now . toLocaleTimeString ( 'en-US' , { hour12 : false });
}
// === Connection Status ===
function updateConnectionStatus ( status , text ) {
const badge = document . getElementById ( 'connectionBadge' );
const textEl = document . getElementById ( 'connectionText' );
textEl . textContent = text ;
badge . className = 'connection-badge' ;
if ( status === 'offline' ) badge . classList . add ( 'offline' );
else if ( status === 'waiting' ) badge . classList . add ( 'warning' );
// 'connected' uses default (green)
}
// === Waveform Drawing ===
function initWaveforms () {
[ 'hr' , 'spo2' , 'bp' , 'rr' , 'temp' , 'map' ]. forEach ( key => {
const canvas = document . getElementById ( `waveform- ${ key } ` );
if ( canvas ) {
const ctx = canvas . getContext ( '2d' );
// Set canvas resolution
const rect = canvas . parentElement . getBoundingClientRect ();
canvas . width = rect . width * 2 ;
canvas . height = rect . height * 2 ;
ctx . scale ( 2 , 2 );
waveformCanvases [ key ] = { canvas , ctx , width : rect . width , height : rect . height };
waveformData [ key ] = [];
}
});
}
function pushWaveformPoint ( key , value ) {
if ( ! waveformData [ key ]) waveformData [ key ] = [];
waveformData [ key ]. push ( value !== null && value !== undefined ? value : null );
if ( waveformData [ key ]. length > 100 ) waveformData [ key ]. shift ();
}
const WAVEFORM_COLORS = {
hr : '#ef4444' , spo2 : '#06b6d4' , bp : '#a855f7' ,
rr : '#22c55e' , temp : '#f97316' , map : '#ec4899'
};
function drawWaveform ( key ) {
const wf = waveformCanvases [ key ];
if ( ! wf ) return ;
const { ctx , width , height } = wf ;
const data = waveformData [ key ] || [];
const color = WAVEFORM_COLORS [ key ];
ctx . clearRect ( 0 , 0 , width , height );
if ( data . length < 2 ) {
// Draw flat line
ctx . beginPath ();
ctx . strokeStyle = ` ${ color } 33` ;
ctx . lineWidth = 1 ;
ctx . moveTo ( 0 , height / 2 );
ctx . lineTo ( width , height / 2 );
ctx . stroke ();
return ;
}
// Filter valid values for min/max
const validValues = data . filter ( v => v !== null );
if ( validValues . length === 0 ) return ;
const min = Math . min (... validValues ) * 0.9 ;
const max = Math . max (... validValues ) * 1.1 ;
const range = max - min || 1 ;
// Draw gradient fill
const gradient = ctx . createLinearGradient ( 0 , 0 , 0 , height );
gradient . addColorStop ( 0 , ` ${ color } 20` );
gradient . addColorStop ( 1 , ` ${ color } 00` );
ctx . beginPath ();
ctx . moveTo ( 0 , height );
let firstPoint = true ;
const step = width / ( data . length - 1 );
data . forEach (( v , i ) => {
if ( v === null ) return ;
const x = i * step ;
const y = height - (( v - min ) / range ) * ( height * 0.8 ) - height * 0.1 ;
if ( firstPoint ) {
ctx . moveTo ( x , height );
ctx . lineTo ( x , y );
firstPoint = false ;
} else {
ctx . lineTo ( x , y );
}
});
// Close fill path
const lastValidIdx = data . length - 1 - [... data ]. reverse (). findIndex ( v => v !== null );
ctx . lineTo ( lastValidIdx * step , height );
ctx . closePath ();
ctx . fillStyle = gradient ;
ctx . fill ();
// Draw line
ctx . beginPath ();
firstPoint = true ;
data . forEach (( v , i ) => {
if ( v === null ) return ;
const x = i * step ;
const y = height - (( v - min ) / range ) * ( height * 0.8 ) - height * 0.1 ;
if ( firstPoint ) {
ctx . moveTo ( x , y );
firstPoint = false ;
} else {
ctx . lineTo ( x , y );
}
});
ctx . strokeStyle = color ;
ctx . lineWidth = 2 ;
ctx . lineJoin = 'round' ;
ctx . lineCap = 'round' ;
ctx . stroke ();
// Glow effect
ctx . strokeStyle = ` ${ color } 40` ;
ctx . lineWidth = 6 ;
ctx . stroke ();
// Draw latest point dot
if ( validValues . length > 0 ) {
const lastVal = validValues [ validValues . length - 1 ];
const lastIdx = data . lastIndexOf ( lastVal );
const x = lastIdx * step ;
const y = height - (( lastVal - min ) / range ) * ( height * 0.8 ) - height * 0.1 ;
ctx . beginPath ();
ctx . arc ( x , y , 4 , 0 , Math . PI * 2 );
ctx . fillStyle = color ;
ctx . fill ();
ctx . beginPath ();
ctx . arc ( x , y , 8 , 0 , Math . PI * 2 );
ctx . fillStyle = ` ${ color } 30` ;
ctx . fill ();
}
}
let waveformPhase = 0 ;
function generateIdleWaveform ( key , type ) {
const data = waveformData [ key ] || [];
if ( data . some ( v => v !== null )) return ; // Only show idle for no-data
// Generate synthetic waveform for visual appeal
const points = 100 ;
waveformData [ key ] = [];
for ( let i = 0 ; i < points ; i ++ ) {
const t = ( i / points ) * Math . PI * 4 + waveformPhase ;
let val ;
if ( type === 'ecg' ) {
// ECG-like pattern
const phase = ( t % ( Math . PI * 2 )) / ( Math . PI * 2 );
if ( phase > 0.35 && phase < 0.40 ) val = 80 + 40 * Math . sin (( phase - 0.35 ) * Math . PI / 0.05 );
else if ( phase > 0.40 && phase < 0.45 ) val = 80 - 15 * Math . sin (( phase - 0.40 ) * Math . PI / 0.05 );
else val = 72 + Math . sin ( t * 0.5 ) * 2 ;
} else if ( type === 'sine' ) {
val = 50 + Math . sin ( t ) * 20 ;
} else {
val = 50 + Math . sin ( t * 0.8 ) * 15 + Math . sin ( t * 2.1 ) * 5 ;
}
waveformData [ key ]. push ( val );
}
}
function animateWaveforms () {
waveformPhase += 0.05 ;
// Generate idle waveforms for cards with no data
generateIdleWaveform ( 'hr' , 'ecg' );
generateIdleWaveform ( 'spo2' , 'sine' );
generateIdleWaveform ( 'bp' , 'sine' );
generateIdleWaveform ( 'rr' , 'sine' );
generateIdleWaveform ( 'temp' , 'sine' );
generateIdleWaveform ( 'map' , 'sine' );
Object . keys ( waveformCanvases ). forEach ( drawWaveform );
requestAnimationFrame ( animateWaveforms );
}
// === Handle window resize ===
window . addEventListener ( 'resize' , () => {
initWaveforms ();
});
</ script >
</ body >
</ html >