HealthTrack
Track your family's health in one place. Blood pressure, medicines, weight & more.
Your data is stored securely in Firebase.
First sign-in creates the admin account.
Log Morning BP
Enter your blood pressure reading
Switch Profile
Add Family Member
They'll sign in with their Google account. Add their details here first.
Manage Access
Delete Profile
/* ===================== BOOT ===================== */ function bootApp(){ document.getElementById('auth-screen').style.display='none'; document.getElementById('main-app').style.display='flex'; const u=state.currentUser; document.getElementById('top-avatar').textContent=u.init; document.getElementById('top-avatar').style.background=u.color; document.getElementById('top-name').textContent=u.name.split(' ')[0]; document.getElementById('s-uname').textContent=u.name; document.getElementById('s-email').textContent=u.email; if(u.isAdmin){ document.getElementById('admin-badge').style.display='inline'; document.getElementById('admin-tab').style.display='inline'; } initToday(); renderCustomFields(); } /* ===================== HELPERS ===================== */ function todayKey(){return new Date().toISOString().slice(0,10)} function activeProfileId(){return state.viewingProfileId||state.currentUser.uid} function getProfileData(pid,key){ if(!state.data[pid])state.data[pid]={}; const k=key||todayKey(); if(!state.data[pid][k])state.data[pid][k]={}; return state.data[pid][k]; } function saveProfileData(pid,dayKey,val){ if(!state.data[pid])state.data[pid]={}; state.data[pid][dayKey]=val; } function getBPClass(sys,dia){if(sys>=140||dia>=90)return'high';if(sys>=130||dia>=80)return'warn';return'ok'} /* ===================== TODAY ===================== */ function initToday(){ const d=new Date(); const days=['Sunday','Monday','Tuesday','Wednesday','Thursday','Friday','Saturday']; const months=['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec']; document.getElementById('today-date').innerHTML= days[d.getDay()]+', '+d.getDate()+' '+months[d.getMonth()]+' '+d.getFullYear()+ 'Today'; const isThursday=d.getDay()===4; document.getElementById('weekly-section').style.display=isThursday?'block':'none'; updateBPDisplay(); updateMedDisplay(); renderCustomSection(); checkStreak(); } function checkStreak(){ let streak=0; for(let i=1;i<=30;i++){ const dt=new Date();dt.setDate(dt.getDate()-i); const k=dt.toISOString().slice(0,10); const d=(state.data[activeProfileId()]||{})[k]||{}; if(d.morning&&d.evening)streak++;else break; } const banner=document.getElementById('streak-banner'); if(streak>0){banner.style.display='flex';document.getElementById('streak-text').innerHTML=`${streak}-day streak! Keep logging daily`;} else banner.style.display='none'; } /* ===================== BP ===================== */ function openBP(slot){ bpSlot=slot; document.getElementById('bp-sheet-title').textContent='Log '+(slot==='morning'?'Morning':'Evening')+' BP'; ['bp-sys','bp-dia','bp-pulse'].forEach(id=>document.getElementById(id).value=''); openSheet('bp-sheet'); setTimeout(()=>document.getElementById('bp-sys').focus(),300); } function saveBP(){ const sys=parseInt(document.getElementById('bp-sys').value)||0; const dia=parseInt(document.getElementById('bp-dia').value)||0; const pulse=parseInt(document.getElementById('bp-pulse').value)||0; if(!sys||!dia){alert('Please enter systolic and diastolic.');return} const pid=activeProfileId(); const k=todayKey(); const d=getProfileData(pid,k); d[bpSlot]={sys,dia,pulse,time:new Date().toLocaleTimeString([],{hour:'2-digit',minute:'2-digit'})}; saveProfileData(pid,k,d); closeSheet('bp-sheet'); updateBPDisplay(); checkHighBP(sys,dia); renderHistory(); } function checkHighBP(sys,dia){ const b=document.getElementById('high-bp-banner'); b.style.display=(sys>=140||dia>=90)?'flex':'none'; } function updateBPDisplay(){ const d=getProfileData(activeProfileId()); ['morning','evening'].forEach(slot=>{ const el=document.getElementById('bp-'+slot+'-slot'); const r=d[slot]; if(r){ const cls=getBPClass(r.sys,r.dia); el.innerHTML=`
${r.sys}/${r.dia}
${r.pulse?r.pulse+' bpm ยท ':''}${r.time}
`; } else { el.innerHTML=``; } }); } /* ===================== MEDICINES ===================== */ function toggleMed(type){ const pid=activeProfileId();const k=todayKey(); const d=getProfileData(pid,k); d['med_'+type]=!d['med_'+type]; saveProfileData(pid,k,d); updateMedDisplay(); } function updateMedDisplay(){ const d=getProfileData(activeProfileId()); ['bp','mv'].forEach(type=>{ const btn=document.getElementById('med-'+type); const icon=document.getElementById('med-'+type+'-icon'); const on=!!d['med_'+type]; btn.classList.toggle('on',on); icon.textContent=on?'โœ“':''; }); } /* ===================== WEEKLY ===================== */ function saveWeekly(){ const w=document.getElementById('w-weight').value; const wt=document.getElementById('w-waist').value; if(!w&&!wt){alert('Enter at least one value.');return} const pid=activeProfileId();const k=todayKey(); const d=getProfileData(pid,k); d.weekly={weight:parseFloat(w)||null,waist:parseFloat(wt)||null}; saveProfileData(pid,k,d); document.getElementById('weekly-card').innerHTML=`
โœ… Weekly log saved! ${w?'Weight: '+w+'kg ':''}${wt?'Waist: '+wt+'"':''}
`; } /* ===================== CUSTOM FIELDS ===================== */ function renderCustomSection(){ const cfs=state.customFields; const sec=document.getElementById('custom-section'); if(!cfs.length){sec.innerHTML='';return} const pid=activeProfileId();const k=todayKey(); const d=getProfileData(pid,k); let html='
Custom
'; cfs.forEach((f,i)=>{ const v=(d.custom||{})[i]||''; html+=`
๐Ÿ“Œ
${f.name}
${f.freq} ยท ${f.unit}
`; }); html+='
'; sec.innerHTML=html; } function saveCustomVal(i,v){ const pid=activeProfileId();const k=todayKey(); const d=getProfileData(pid,k); if(!d.custom)d.custom={}; d.custom[i]=v; saveProfileData(pid,k,d); } function renderCustomFields(){ const cfs=state.customFields; const el=document.getElementById('cf-list'); if(!cfs.length){el.innerHTML='

No custom fields

';return} let html=''; cfs.forEach((f,i)=>{ html+=`
`; }); el.innerHTML=html; } function addCustomField(){ state.customFields.push({name:'',unit:'',freq:'daily'}); renderCustomFields();renderCustomSection(); } function updateCF(i,k,v){state.customFields[i][k]=v;renderCustomSection();} function deleteCF(i){state.customFields.splice(i,1);renderCustomFields();renderCustomSection();} /* ===================== HISTORY ===================== */ function renderHistory(){ const pid=activeProfileId(); const list=document.getElementById('history-list'); const today=new Date();let html=''; for(let i=0;i<14;i++){ const dt=new Date(today);dt.setDate(today.getDate()-i); const k=dt.toISOString().slice(0,10); const d=(state.data[pid]||{})[k]||{}; const label=i===0?'Today':i===1?'Yesterday':dt.toLocaleDateString([],{weekday:'short',month:'short',day:'numeric'}); const hasSomething=d.morning||d.evening||d.med_bp||d.med_mv||d.weekly; if(i>0&&!hasSomething)continue; html+=`
${label}
Morning BP
${d.morning?`${d.morning.sys}/${d.morning.dia}`:'โ€”'}
Evening BP
${d.evening?`${d.evening.sys}/${d.evening.dia}`:'โ€”'}
BP Tablet
${d.med_bp?'โœ…':'โ€”'}
Multivitamin
${d.med_mv?'โœ…':'โ€”'}
${d.weekly?`
Weight
${d.weekly.weight?d.weekly.weight+' kg':'โ€”'}
Waist
${d.weekly.waist?d.weekly.waist+'"':'โ€”'}
`:''}
`; } list.innerHTML=html||'
No data logged yet
'; } /* ===================== TRENDS ===================== */ function renderTrends(){ const pid=activeProfileId(); const readings=[];const wReadings=[]; const today=new Date(); for(let i=13;i>=0;i--){ const dt=new Date(today);dt.setDate(today.getDate()-i); const k=dt.toISOString().slice(0,10); const d=(state.data[pid]||{})[k]||{}; const lbl=dt.toLocaleDateString([],{weekday:'short'}).slice(0,2); if(d.morning)readings.push({lbl,v:d.morning.sys,cls:getBPClass(d.morning.sys,d.morning.dia)}); if(d.weekly&&d.weekly.weight)wReadings.push({lbl:k.slice(5),v:d.weekly.weight}); } // sample data if empty const bpData=readings.length?readings:[ {lbl:'M',v:138,cls:'warn'},{lbl:'T',v:142,cls:'high'},{lbl:'W',v:135,cls:'warn'}, {lbl:'T',v:130,cls:'warn'},{lbl:'F',v:128,cls:'ok'},{lbl:'S',v:132,cls:'warn'},{lbl:'S',v:125,cls:'ok'} ]; const wData=wReadings.length?wReadings:[ {lbl:'Wk1',v:72.5},{lbl:'Wk2',v:72.1},{lbl:'Wk3',v:71.8},{lbl:'Wk4',v:71.3} ]; const maxBP=Math.max(...bpData.map(r=>r.v),160); let html=''; bpData.forEach(r=>{ const pct=Math.round((r.v/maxBP)*100); html+=`
${r.lbl}
${r.v}
`; }); document.getElementById('bp-trend-chart').innerHTML=html; const maxW=Math.max(...wData.map(r=>r.v))+2; let wHtml=''; wData.forEach(r=>{ const pct=Math.round((r.v/maxW)*100); wHtml+=`
${r.lbl}
${r.v}
`; }); document.getElementById('weight-chart').innerHTML=wHtml; } /* ===================== PROFILES ===================== */ function renderProfileSheet(){ const u=state.currentUser; const perms=state.permissions[u.uid]||{}; const accessible=[u.uid]; if(u.isAdmin||perms.canViewAll){ Object.keys(state.profiles).forEach(id=>{if(!accessible.includes(id))accessible.push(id);}); } else if(perms.canView){ perms.canView.forEach(id=>{if(!accessible.includes(id))accessible.push(id);}); } let html=''; accessible.forEach(pid=>{ const p=state.profiles[pid]; if(!p)return; const isCurrent=(state.viewingProfileId||u.uid)===pid; html+=`
${p.init||p.name[0]}
${p.name}${pid===u.uid?' (Me)':''}
${p.email||p.relation||''}
${isCurrent?'โœ“':''}
`; }); if(u.isAdmin){ html+=`
+
Add family member
`; } document.getElementById('profile-list').innerHTML=html; } function switchViewingProfile(pid){ const u=state.currentUser; if(pid===u.uid){ state.viewingProfileId=null; document.getElementById('viewing-bar').style.display='none'; } else { state.viewingProfileId=pid; const p=state.profiles[pid]; document.getElementById('viewing-name').textContent=p.name; document.getElementById('viewing-bar').style.display='flex'; } closeSheet('profile-sheet'); initToday(); renderHistory(); } function stopViewing(){ state.viewingProfileId=null; document.getElementById('viewing-bar').style.display='none'; initToday();renderHistory(); } /* ===================== ADMIN ===================== */ function renderAdmin(){ const pids=Object.keys(state.profiles); document.getElementById('stat-members').textContent=pids.length; const k=todayKey(); let logs=0; pids.forEach(pid=>{const d=(state.data[pid]||{})[k]||{};if(d.morning||d.evening||d.med_bp||d.med_mv)logs++;}); document.getElementById('stat-logs').textContent=logs; let html=''; pids.forEach(pid=>{ const p=state.profiles[pid]; const perms=state.permissions[pid]||{}; const isAdmin=p.isAdmin; html+=`
${p.init||p.name[0]}
${p.name}${isAdmin?' ๐Ÿ‘‘':''}
${p.email||p.relation||'Family member'}
${isAdmin?'ADMIN':''}
${!isAdmin?``:''} ${!isAdmin?``:''}
`; }); html+=``; document.getElementById('members-list').innerHTML=html; } function adminViewMember(pid){ switchViewingProfile(pid); switchTab('today',document.querySelector('.tab')); } function openPermSheet(pid){ pendingPermId=pid; const p=state.profiles[pid]; const perms=state.permissions[pid]||{canView:[]}; document.getElementById('perm-sheet-title').textContent='Access for '+p.name; document.getElementById('perm-sheet-sub').textContent='Choose which profiles '+p.name+' can view.'; const others=Object.keys(state.profiles).filter(id=>id!==pid); let html=''; others.forEach(oid=>{ const op=state.profiles[oid]; const hasAccess=perms.canViewAll||(perms.canView||[]).includes(oid); html+=`
${op.init||op.name[0]}
${op.name}
`; }); document.getElementById('perm-list').innerHTML=html||'

No other members yet

'; openSheet('perm-sheet'); } function savePermissions(){ if(!pendingPermId)return; const others=Object.keys(state.profiles).filter(id=>id!==pendingPermId); const canView=others.filter(oid=>{ const t=document.getElementById('perm-toggle-'+oid); return t&&t.classList.contains('on'); }); state.permissions[pendingPermId]={canView,canViewAll:canView.length===others.length}; closeSheet('perm-sheet'); renderAdmin(); } function openDeleteSheet(pid){ pendingDeleteId=pid; const p=state.profiles[pid]; document.getElementById('delete-sheet-sub').textContent=`You are about to delete ${p.name}'s profile and all their health data.`; openSheet('delete-sheet'); } function confirmDelete(){ if(!pendingDeleteId)return; delete state.profiles[pendingDeleteId]; delete state.data[pendingDeleteId]; delete state.permissions[pendingDeleteId]; closeSheet('delete-sheet'); renderAdmin(); } /* ===================== ADD MEMBER ===================== */ function addMember(){ const name=document.getElementById('new-member-name').value.trim(); if(!name){alert('Enter a name.');return} const relation=document.getElementById('new-member-relation').value; const color=document.getElementById('new-member-color').value; const uid='member_'+Date.now(); state.profiles[uid]={uid,name,relation,email:'',color,init:name[0].toUpperCase(),isAdmin:false}; state.permissions[uid]={canView:[],canViewAll:false}; closeSheet('add-member-sheet'); document.getElementById('new-member-name').value=''; renderAdmin(); } /* ===================== TABS ===================== */ function switchTab(tab,el){ document.querySelectorAll('.tab').forEach(t=>t.classList.remove('active')); if(el)el.classList.add('active'); ['today','history','trends','settings','admin'].forEach(t=>{ const el=document.getElementById('tab-'+t); if(el)el.style.display=t===tab?'block':'none'; }); state.activeTab=tab; if(tab==='trends')renderTrends(); if(tab==='history')renderHistory(); if(tab==='admin')renderAdmin(); if(tab==='settings'){renderCustomFields();} if(tab==='today')initToday(); } /* ===================== SHEETS ===================== */ function openSheet(id){ if(id==='profile-sheet')renderProfileSheet(); document.getElementById(id).classList.add('open'); } function closeSheet(id){document.getElementById(id).classList.remove('open')} document.addEventListener('click',e=>{ if(e.target.classList.contains('overlay'))e.target.classList.remove('open'); }); /* ===================== PWA INSTALL ===================== */ let deferredPrompt=null; window.addEventListener('beforeinstallprompt',e=>{ e.preventDefault();deferredPrompt=e; const b=document.createElement('button'); b.className='banner blue';b.style.cssText='width:100%;border:none;cursor:pointer;text-align:left;margin-bottom:12px'; b.innerHTML='๐Ÿ“ฒAdd to Home Screen for quick access'; b.onclick=()=>{deferredPrompt.prompt();b.remove()}; const c=document.getElementById('content');if(c.firstChild)c.insertBefore(b,c.firstChild); });