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 ;
max-width : 1400 px ;
margin : 0 auto ;
padding : 24 px ;
min-height : 100 vh ;
display : flex ;
flex-direction : column ;
2026-07-13 15:17:30 +05:30
width : 100 % ;
2026-07-08 14:14:03 +05:30
}
/* === Header === */
. 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
background : var ( -- bg - card );
border : 1 px solid var ( -- border - color );
2026-07-13 15:17:30 +05:30
border-radius : 20 px ;
box-shadow :
0 4 px 6 px -1 px rgba ( 0 , 0 , 0 , 0.02 ),
0 2 px 4 px -1 px rgba ( 0 , 0 , 0 , 0.01 ),
0 10 px 15 px -3 px rgba ( 0 , 0 , 0 , 0.03 );
2026-07-08 14:14:03 +05:30
margin-bottom : 24 px ;
}
. header-left {
display : flex ;
align-items : center ;
gap : 16 px ;
}
. header-icon {
width : 48 px ;
height : 48 px ;
2026-07-13 15:17:30 +05:30
border-radius : 14 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 ;
font-size : 24 px ;
2026-07-13 15:17:30 +05:30
box-shadow : 0 4 px 14 px rgba ( 8 , 145 , 178 , 0.2 );
2026-07-08 14:14:03 +05:30
}
. header-title {
2026-07-13 15:17:30 +05:30
font-size : 20 px ;
2026-07-08 14:14:03 +05:30
font-weight : 700 ;
letter-spacing : -0.5 px ;
2026-07-13 15:17:30 +05:30
color : var ( -- text - primary );
2026-07-08 14:14:03 +05:30
}
. header-subtitle {
font-size : 13 px ;
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 ;
gap : 16 px ;
}
/* Audio Alert Toggle */
. audio-toggle {
display : flex ;
align-items : center ;
gap : 8 px ;
2026-07-13 15:17:30 +05:30
padding : 8 px 16 px ;
2026-07-08 14:14:03 +05:30
border-radius : 20 px ;
font-size : 13 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 ;
gap : 8 px ;
padding : 8 px 16 px ;
border-radius : 20 px ;
font-size : 13 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 {
width : 8 px ;
height : 8 px ;
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 ); }
50 % { opacity : 0.7 ; box-shadow : 0 0 0 6 px rgba ( 34 , 197 , 94 , 0 ); }
}
. time-display {
font-family : 'JetBrains Mono' , monospace ;
font-size : 14 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 ;
gap : 8 px ;
margin-right : 8 px ;
}
. 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 );
padding : 8 px 16 px ;
border-radius : 20 px ;
font-size : 13 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 15:17:30 +05:30
box-shadow : 0 2 px 4 px rgba ( 0 , 0 , 0 , 0.02 );
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 15:17:30 +05:30
background : linear-gradient ( 135 deg , rgba ( 8 , 145 , 178 , 0.04 ), rgba ( 124 , 58 , 237 , 0.02 ));
border : 1 px solid rgba ( 8 , 145 , 178 , 0.12 );
border-radius : 20 px ;
2026-07-08 14:14:03 +05:30
margin-bottom : 24 px ;
overflow : hidden ;
2026-07-13 15:17:30 +05:30
box-shadow : 0 4 px 15 px -3 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 );
}
. setup-guide . hidden {
max-height : 0 ;
margin-bottom : 0 ;
border-color : transparent ;
opacity : 0 ;
}
. 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 );
grid-template-rows : auto auto ;
2026-07-13 15:17:30 +05:30
gap : 24 px ;
2026-07-08 14:14:03 +05:30
flex : 1 ;
}
. vital-card {
background : var ( -- bg - card );
border : 1 px solid var ( -- border - color );
2026-07-13 15:17:30 +05:30
border-radius : 24 px ;
padding : 24 px ;
2026-07-08 14:14:03 +05:30
position : relative ;
overflow : hidden ;
2026-07-13 15:17:30 +05:30
box-shadow :
0 4 px 6 px -1 px rgba ( 0 , 0 , 0 , 0.02 ),
0 2 px 4 px -1 px rgba ( 0 , 0 , 0 , 0.01 ),
0 15 px 30 px -10 px rgba ( 0 , 0 , 0 , 0.04 );
2026-07-08 14:14:03 +05:30
transition : all 0.4 s cubic-bezier ( 0.16 , 1 , 0.3 , 1 );
display : flex ;
flex-direction : column ;
}
. vital-card : hover {
background : var ( -- bg - card - hover );
2026-07-13 15:17:30 +05:30
transform : translateY ( -4 px );
box-shadow :
0 20 px 40 px -15 px rgba ( 0 , 0 , 0 , 0.08 ),
0 0 0 1 px rgba ( 0 , 0 , 0 , 0.02 );
2026-07-08 14:14:03 +05:30
}
2026-07-13 15:23:10 +05:30
. vital-card . hr : hover { border-color : rgba ( 220 , 38 , 38 , 0.3 ); }
. vital-card . spo2 : hover { border-color : rgba ( 8 , 145 , 178 , 0.3 ); }
. vital-card . bp : hover { border-color : rgba ( 124 , 58 , 237 , 0.3 ); }
. vital-card . rr : hover { border-color : rgba ( 22 , 163 , 74 , 0.3 ); }
. vital-card . temp : hover { border-color : rgba ( 234 , 88 , 12 , 0.3 ); }
. vital-card . map-card : hover { border-color : rgba ( 219 , 39 , 119 , 0.3 ); }
2026-07-08 14:14:03 +05:30
. vital-card :: before {
content : '' ;
position : absolute ;
top : 0 ;
left : 0 ;
right : 0 ;
2026-07-13 15:17:30 +05:30
height : 4 px ;
border-radius : 24 px 24 px 0 0 ;
2026-07-08 14:14:03 +05:30
}
. vital-card . hr :: before { background : linear-gradient ( 90 deg , var ( -- hr - color ), transparent ); }
. vital-card . spo2 :: before { background : linear-gradient ( 90 deg , var ( -- spo2 - color ), transparent ); }
. vital-card . bp :: before { background : linear-gradient ( 90 deg , var ( -- bp - color ), transparent ); }
. vital-card . rr :: before { background : linear-gradient ( 90 deg , var ( -- rr - color ), transparent ); }
. vital-card . temp :: before { background : linear-gradient ( 90 deg , var ( -- temp - color ), transparent ); }
. vital-card . map-card :: before { background : linear-gradient ( 90 deg , var ( -- map - color ), transparent ); }
/* Glow effect behind cards */
. vital-card :: after {
content : '' ;
position : absolute ;
top : 50 % ;
left : 50 % ;
width : 200 px ;
height : 200 px ;
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 15:17:30 +05:30
opacity : 0.4 ;
2026-07-08 14:14:03 +05:30
}
. vital-card . has-value : hover :: after {
2026-07-13 15:17:30 +05:30
opacity : 0.7 ;
2026-07-08 14:14:03 +05:30
}
. card-header {
display : flex ;
align-items : center ;
justify-content : space-between ;
2026-07-13 15:17:30 +05:30
margin-bottom : 12 px ;
2026-07-08 14:14:03 +05:30
position : relative ;
z-index : 1 ;
}
. card-label {
display : flex ;
align-items : center ;
gap : 10 px ;
}
. card-icon {
2026-07-13 15:17:30 +05:30
width : 38 px ;
height : 38 px ;
border-radius : 12 px ;
2026-07-08 14:14:03 +05:30
display : flex ;
align-items : center ;
justify-content : center ;
font-size : 18 px ;
2026-07-13 15:17:30 +05:30
box-shadow : 0 2 px 5 px rgba ( 0 , 0 , 0 , 0.02 );
2026-07-08 14:14:03 +05:30
}
2026-07-13 15:17:30 +05:30
. vital-card . hr . card-icon { background : rgba ( 220 , 38 , 38 , 0.08 ); }
. vital-card . spo2 . card-icon { background : rgba ( 8 , 145 , 178 , 0.08 ); }
. vital-card . bp . card-icon { background : rgba ( 124 , 58 , 237 , 0.08 ); }
. vital-card . rr . card-icon { background : rgba ( 22 , 163 , 74 , 0.08 ); }
. vital-card . temp . card-icon { background : rgba ( 234 , 88 , 12 , 0.08 ); }
. vital-card . map-card . card-icon { background : rgba ( 219 , 39 , 119 , 0.08 ); }
2026-07-08 14:14:03 +05:30
. card-name {
font-size : 13 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 15:17:30 +05:30
letter-spacing : 0.8 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 {
padding : 4 px 10 px ;
border-radius : 8 px ;
font-size : 11 px ;
2026-07-13 15:17:30 +05:30
font-weight : 700 ;
2026-07-08 14:14:03 +05:30
text-transform : uppercase ;
letter-spacing : 0.5 px ;
}
. status-badge . normal {
2026-07-13 15:17:30 +05:30
background : rgba ( 22 , 163 , 74 , 0.08 );
2026-07-08 14:14:03 +05:30
color : var ( -- status - ok );
2026-07-13 15:17:30 +05:30
border : 1 px solid rgba ( 22 , 163 , 74 , 0.15 );
2026-07-08 14:14:03 +05:30
}
. status-badge . warning {
2026-07-13 15:17:30 +05:30
background : rgba ( 202 , 138 , 4 , 0.08 );
2026-07-08 14:14:03 +05:30
color : var ( -- status - warn );
2026-07-13 15:17:30 +05:30
border : 1 px solid rgba ( 202 , 138 , 4 , 0.15 );
2026-07-08 14:14:03 +05:30
animation : warningFlash 2 s ease-in-out infinite ;
}
. status-badge . critical {
2026-07-13 15:17:30 +05:30
background : rgba ( 220 , 38 , 38 , 0.08 );
2026-07-08 14:14:03 +05:30
color : var ( -- status - critical );
2026-07-13 15:17:30 +05:30
border : 1 px solid rgba ( 220 , 38 , 38 , 0.15 );
2026-07-08 14:14:03 +05:30
animation : criticalFlash 1 s ease-in-out infinite ;
}
. status-badge . offline {
2026-07-13 15:17:30 +05:30
background : rgba ( 100 , 116 , 139 , 0.06 );
2026-07-08 14:14:03 +05:30
color : var ( -- status - offline );
2026-07-13 15:17:30 +05:30
border : 1 px solid rgba ( 100 , 116 , 139 , 0.1 );
2026-07-08 14:14:03 +05:30
}
@ keyframes warningFlash {
0 %, 100 % { opacity : 1 ; }
50 % { opacity : 0.6 ; }
}
@ keyframes criticalFlash {
0 %, 100 % { opacity : 1 ; }
50 % { opacity : 0.4 ; }
}
/* === Vital Value Display === */
. vital-value-container {
flex : 1 ;
display : flex ;
align-items : center ;
justify-content : center ;
position : relative ;
z-index : 1 ;
2026-07-13 15:17:30 +05:30
min-height : 110 px ;
2026-07-08 14:14:03 +05:30
}
. vital-value {
font-family : 'JetBrains Mono' , monospace ;
font-size : 64 px ;
font-weight : 700 ;
line-height : 1 ;
letter-spacing : -2 px ;
transition : all 0.5 s cubic-bezier ( 0.16 , 1 , 0.3 , 1 );
}
2026-07-13 15:17:30 +05:30
. vital-card . hr . vital-value { color : var ( -- hr - color ); }
. vital-card . spo2 . vital-value { color : var ( -- spo2 - color ); }
. vital-card . bp . vital-value { color : var ( -- bp - color ); }
. vital-card . rr . vital-value { color : var ( -- rr - color ); }
. vital-card . temp . vital-value { color : var ( -- temp - color ); }
. vital-card . map-card . vital-value { color : var ( -- map - color ); }
2026-07-08 14:14:03 +05:30
. vital-value . no-data {
font-size : 48 px ;
color : var ( -- text - dim ) !important ;
}
. vital-value . unit {
font-size : 20 px ;
2026-07-13 15:17:30 +05:30
font-weight : 500 ;
2026-07-08 14:14:03 +05:30
letter-spacing : 0 ;
margin-left : 4 px ;
2026-07-13 15:17:30 +05:30
color : var ( -- text - secondary );
2026-07-08 14:14:03 +05:30
}
. vital-value . slash {
font-size : 48 px ;
2026-07-13 15:17:30 +05:30
opacity : 0.3 ;
2026-07-08 14:14:03 +05:30
margin : 0 2 px ;
2026-07-13 15:17:30 +05:30
color : var ( -- text - secondary );
2026-07-08 14:14:03 +05:30
}
. vital-value-update {
animation : valueFlash 0.6 s ease-out ;
}
@ keyframes valueFlash {
2026-07-13 15:17:30 +05:30
0 % { transform : scale ( 1.06 ); filter : brightness ( 1.2 ); }
2026-07-08 14:14:03 +05:30
100 % { transform : scale ( 1 ); filter : brightness ( 1 ); }
}
/* === Waveform Canvas === */
. waveform-container {
2026-07-13 15:17:30 +05:30
height : 55 px ;
2026-07-08 14:14:03 +05:30
margin-top : 12 px ;
position : relative ;
z-index : 1 ;
overflow : hidden ;
2026-07-13 15:17:30 +05:30
border-radius : 12 px ;
background : rgba ( 0 , 0 , 0 , 0.01 );
border : 1 px solid rgba ( 0 , 0 , 0 , 0.02 );
2026-07-08 14:14:03 +05:30
}
. waveform-container canvas {
width : 100 % ;
height : 100 % ;
display : block ;
}
/* === Normal Range Footer === */
. card-footer {
display : flex ;
align-items : center ;
justify-content : space-between ;
2026-07-13 15:17:30 +05:30
margin-top : 14 px ;
2026-07-08 14:14:03 +05:30
padding-top : 12 px ;
border-top : 1 px solid var ( -- border - color );
position : relative ;
z-index : 1 ;
}
. normal-range {
2026-07-13 15:17:30 +05:30
font-size : 11 px ;
color : var ( -- text - secondary );
font-weight : 600 ;
2026-07-08 14:14:03 +05:30
}
. last-update {
font-size : 11 px ;
color : var ( -- text - dim );
font-family : 'JetBrains Mono' , monospace ;
2026-07-13 15:17:30 +05:30
font-weight : 500 ;
2026-07-08 14:14:03 +05:30
}
/* === Info Bar === */
. info-bar {
display : flex ;
align-items : center ;
justify-content : space-between ;
padding : 16 px 24 px ;
background : var ( -- bg - card );
border : 1 px solid var ( -- border - color );
2026-07-13 15:17:30 +05:30
border-radius : 20 px ;
box-shadow :
0 4 px 6 px -1 px rgba ( 0 , 0 , 0 , 0.02 ),
0 2 px 4 px -1 px rgba ( 0 , 0 , 0 , 0.01 ),
0 10 px 15 px -3 px rgba ( 0 , 0 , 0 , 0.03 );
margin-top : 24 px ;
2026-07-08 14:14:03 +05:30
flex-wrap : wrap ;
gap : 12 px ;
}
. info-item {
display : flex ;
align-items : center ;
gap : 8 px ;
font-size : 13 px ;
color : var ( -- text - secondary );
2026-07-13 15:17:30 +05:30
font-weight : 500 ;
2026-07-08 14:14:03 +05:30
}
. info-item strong {
color : var ( -- text - primary );
2026-07-13 15:17:30 +05:30
font-weight : 700 ;
2026-07-08 14:14:03 +05:30
}
. info-dot {
width : 6 px ;
height : 6 px ;
border-radius : 50 % ;
2026-07-13 15:17:30 +05:30
background : var ( -- border - color );
2026-07-08 14:14:03 +05:30
}
. model-badge {
padding : 4 px 12 px ;
border-radius : 8 px ;
font-size : 11 px ;
2026-07-13 15:17:30 +05:30
font-weight : 800 ;
2026-07-08 14:14:03 +05:30
letter-spacing : 0.5 px ;
2026-07-13 15:17:30 +05:30
background : linear-gradient ( 135 deg , rgba ( 8 , 145 , 178 , 0.08 ), rgba ( 124 , 58 , 237 , 0.08 ));
2026-07-08 14:14:03 +05:30
color : var ( -- spo2 - color );
2026-07-13 15:17:30 +05:30
border : 1 px solid rgba ( 8 , 145 , 178 , 0.15 );
2026-07-08 14:14:03 +05:30
}
/* === Responsive === */
@ media ( max-width : 1024px ) {
. vitals-grid {
grid-template-columns : repeat ( 2 , 1 fr );
2026-07-13 15:17:30 +05:30
gap : 20 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 15:17:30 +05:30
gap : 16 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 ;
}
. vital-value { font-size : 56 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 ;
justify-content : space-between ;
padding : 16 px 24 px ;
background : var ( -- bg - card );
border : 1 px solid var ( -- border - color );
border-radius : 20 px ;
box-shadow :
0 4 px 6 px -1 px rgba ( 0 , 0 , 0 , 0.02 ),
0 10 px 15 px -3 px rgba ( 0 , 0 , 0 , 0.03 );
margin-bottom : 24 px ;
flex-wrap : wrap ;
gap : 16 px ;
}
. patient-banner-group {
display : flex ;
align-items : center ;
gap : 12 px ;
}
. patient-avatar {
width : 36 px ;
height : 36 px ;
border-radius : 50 % ;
background : rgba ( 8 , 145 , 178 , 0.08 );
color : var ( -- spo2 - color );
display : flex ;
align-items : center ;
justify-content : center ;
}
. patient-avatar svg {
width : 18 px ;
height : 18 px ;
}
. patient-label {
font-size : 11 px ;
color : var ( -- text - secondary );
font-weight : 700 ;
text-transform : uppercase ;
letter-spacing : 0.5 px ;
}
. patient-value-text {
font-size : 14 px ;
color : var ( -- text - primary );
font-weight : 700 ;
}
. patient-banner-divider {
height : 24 px ;
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-08 14:14:03 +05:30
/* === Scrollbar === */
:: -webkit-scrollbar { width : 6 px ; }
:: -webkit-scrollbar-track { background : transparent ; }
2026-07-13 15:17:30 +05:30
:: -webkit-scrollbar-thumb { background : rgba ( 0 , 0 , 0 , 0.1 ); border-radius : 3 px ; }
2026-07-08 14:14:03 +05:30
</ style >
</ head >
< body >
< 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 class = "header-icon" > 🏥</ div >
< 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 >
2026-07-13 15:23:10 +05:30
<!-- Patient Clinical Info Banner -->
< div class = "patient-banner" >
< div class = "patient-banner-group" >
< div class = "patient-avatar" >
< svg viewBox = "0 0 24 24" fill = "none" stroke = "currentColor" stroke-width = "2.5" stroke-linecap = "round" stroke-linejoin = "round" >
< path d = "M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2" />
< circle cx = "12" cy = "7" r = "4" />
</ svg >
</ div >
< div >
< div class = "patient-label" > Active Patient</ div >
< div class = "patient-value-text" id = "bannerPatientName" > John Doe</ div >
</ div >
</ div >
< div class = "patient-banner-divider" ></ div >
< div class = "patient-banner-group" >
< div >
< div class = "patient-label" > Patient ID</ div >
< div class = "patient-value-text" id = "bannerPatientId" > N/A</ div >
</ div >
</ div >
< div class = "patient-banner-divider" ></ div >
< div class = "patient-banner-group" >
< div >
< div class = "patient-label" > Location / Bed</ div >
< div class = "patient-value-text" > ICU - Bed 12</ div >
</ div >
</ div >
< div class = "patient-banner-divider" ></ div >
< div class = "patient-banner-group" >
< div >
< div class = "patient-label" > Monitor Status</ div >
< div class = "patient-value-text" id = "bannerMonitorModel" > CMS8500</ div >
</ div >
</ div >
< div class = "patient-banner-divider" ></ div >
< div class = "patient-banner-group" >
< div >
< div class = "patient-label" > Last Transmission</ div >
< div class = "patient-value-text" id = "bannerLastSeen" > Never</ div >
</ div >
</ div >
</ div >
2026-07-08 14:14:03 +05:30
<!-- Setup Guide -->
< div class = "setup-guide" id = "setupGuide" >
< div class = "setup-guide-header" onclick = "toggleSetupGuide()" >
< div class = "setup-guide-title" >
< span > 📡</ span >
2026-07-11 14:24:14 +05:30
< span > Contec CMS7000PLUS / CMS8500 Setup Guide — Configure Your Monitor</ span >
2026-07-08 14:14:03 +05:30
</ div >
< div class = "setup-guide-toggle" id = "setupToggleText" > ▾ Collapse</ div >
</ div >
< div class = "setup-guide-body" id = "setupGuideBody" >
< div class = "setup-step" >
< div class = "step-number" > 1</ div >
< div class = "step-content" >
2026-07-11 14:24:14 +05:30
< h4 > Connect via Wi-Fi or Ethernet</ h4 >
< p >< strong > Wi-Fi:</ strong > Connect the monitor (e.g. CMS8500) and your PC to the same Wi-Fi network. < br >< strong > Ethernet:</ strong > Connect using an Ethernet cable (direct or switch).</ p >
2026-07-08 14:14:03 +05:30
</ div >
</ div >
< div class = "setup-step" >
< div class = "step-number" > 2</ div >
< div class = "step-content" >
2026-07-11 14:24:14 +05:30
< h4 > Get PC IP Address</ h4 >
< p > Find your PC's IP address on the network (shown in the box below). If using Wi-Fi, use your PC's Wi-Fi IP address.</ p >
2026-07-08 14:14:03 +05:30
</ div >
</ div >
< div class = "setup-step" >
< div class = "step-number" > 3</ div >
< div class = "step-content" >
< h4 > Configure Monitor CMS Settings</ h4 >
2026-07-11 14:24:14 +05:30
< p > On your monitor: Go to < code > System Setup → Network → CMS Settings</ code > . Set < strong > Server IP</ strong > (or CMS IP) to your PC's IP address.</ p >
2026-07-08 14:14:03 +05:30
</ div >
</ div >
< div class = "setup-step" >
< div class = "step-number" > 4</ div >
< div class = "step-content" >
< h4 > Set Server Port</ h4 >
2026-07-11 14:24:14 +05:30
< p > Set Server Port on your monitor to one of the listening ports: < code id = "activePortsList" > Detecting...</ code > (usually < code > 511</ code > or < code > 518</ code > ).</ p >
2026-07-08 14:14:03 +05:30
</ div >
</ div >
< div class = "setup-step" >
< div class = "step-number" > 5</ div >
< div class = "step-content" >
< h4 > Enable CMS Connection</ h4 >
2026-07-11 14:24:14 +05:30
< p > Enable the CMS/Central Monitor connection in the monitor settings. Set the sending interval (e.g. 5 seconds) to start transmitting.</ p >
2026-07-08 14:14:03 +05:30
</ div >
</ div >
< div class = "setup-step" >
< div class = "step-number" > 6</ div >
< div class = "step-content" >
< h4 > Data Will Appear Automatically</ h4 >
2026-07-11 14:24:14 +05:30
< p > Once connected, the monitor will show up in the "Select Monitor" list, and its vitals will display in real-time.</ p >
2026-07-08 14:14:03 +05:30
</ div >
</ div >
< div class = "setup-ip-display" >
< div >
< div class = "setup-ip-label" > 🖥️ Your PC's Detected IP Address (set this on the monitor)</ div >
</ div >
< div class = "setup-ip-value" id = "detectedIp" > Detecting...</ div >
</ div >
</ div >
</ div >
<!-- 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" >
< span class = "normal-range" > Normal: 36.1– 37.2 °C</ span >
< 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 },
temperature : { low : 36.1 , high : 37.2 , critLow : 35.0 , critHigh : 39.0 },
};
// === 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-08 14:14:03 +05:30
// === Initialize ===
document . addEventListener ( 'DOMContentLoaded' , () => {
initWaveforms ();
updateClock ();
setInterval ( updateClock , 1000 );
2026-07-11 14:24:14 +05:30
fetchDevices (). then (() => {
tryWebSocket ();
startPolling ();
});
2026-07-08 14:14:03 +05:30
animateWaveforms ();
fetchNetworkInfo ();
2026-07-11 14:24:14 +05:30
// Periodically refresh the list of active devices
setInterval ( fetchDevices , 4000 );
2026-07-08 14:14:03 +05:30
});
// === 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
if ( ! selectedDeviceId && data . device_id ) {
selectDevice ( data . device_id );
}
if ( data . device_id === selectedDeviceId ) {
updateVitals ( data );
}
// Refresh device list status
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 () {
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
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 ✓' ;
2026-07-11 14:24:14 +05:30
// 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-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>` );
updateVitalCard ( 'temp' , data . temperature , 'temperature' , v => ` ${ v . toFixed ( 1 ) } <span class="unit">°C</span>` );
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 );
pushWaveformPoint ( 'temp' , data . temperature );
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 >