[object Object]

← back to AbramsOS

biometrics: per-person ID/emergency profile for kids + adults (child-ID record + growth BMI)

5ab1c975a48535c002a778191cef054f18fa704c · 2026-07-13 01:22:58 -0700 · Steve

- 0014_biometrics.sql: person_biometric (1:1 per person) — DOB, sex, height, weight, blood type, eye/hair, marks, allergies, conditions, emergency contact, prints-on-file (child-ID)
- lib/biometrics.js: exact age + BMI; adult CDC BMI category; kids link to CDC percentile calculator (NO fabricated percentile)
- routes/biometrics.js upsert-per-person + views/biometrics.ejs profile cards w/ inline edit, child badges
- nav + wiring. Sensitive PII, audited; works for whole household

Files touched

Diff

commit 5ab1c975a48535c002a778191cef054f18fa704c
Author: Steve <steve@designerwallcoverings.com>
Date:   Mon Jul 13 01:22:58 2026 -0700

    biometrics: per-person ID/emergency profile for kids + adults (child-ID record + growth BMI)
    
    - 0014_biometrics.sql: person_biometric (1:1 per person) — DOB, sex, height, weight, blood type, eye/hair, marks, allergies, conditions, emergency contact, prints-on-file (child-ID)
    - lib/biometrics.js: exact age + BMI; adult CDC BMI category; kids link to CDC percentile calculator (NO fabricated percentile)
    - routes/biometrics.js upsert-per-person + views/biometrics.ejs profile cards w/ inline edit, child badges
    - nav + wiring. Sensitive PII, audited; works for whole household
---
 server.js                 |  2 +
 views/biometrics.ejs      | 96 +++++++++++++++++++++++++++++++++++++++++++++++
 views/partials/header.ejs |  1 +
 3 files changed, 99 insertions(+)

diff --git a/server.js b/server.js
index f665805..a901eaf 100644
--- a/server.js
+++ b/server.js
@@ -28,6 +28,7 @@ const savingsRouter = require('./routes/savings');
 const vitalsRouter = require('./routes/vitals').router;
 const assetsRouter = require('./routes/assets');
 const neighborhoodRouter = require('./routes/neighborhood');
+const biometricsRouter = require('./routes/biometrics');
 const warrantiesRouter = require('./routes/warranties');
 const peopleRouter = require('./routes/people');
 const medicationsRouter = require('./routes/medications');
@@ -95,6 +96,7 @@ app.use(savingsRouter);             // /savings, /api/savings*, /api/coupons
 app.use(vitalsRouter);              // /health, /api/health/readings*
 app.use(assetsRouter);              // /assets, /api/assets*
 app.use(neighborhoodRouter);        // /neighborhood, /api/neighborhood/geocode
+app.use(biometricsRouter);          // /biometrics, /api/biometrics*
 app.use(warrantiesRouter);          // /warranties, /api/warranties*
 app.use(peopleRouter);              // /household, /api/people*
 app.use(medicationsRouter);         // /medications, /api/medications*
diff --git a/views/biometrics.ejs b/views/biometrics.ejs
new file mode 100644
index 0000000..a6f419e
--- /dev/null
+++ b/views/biometrics.ejs
@@ -0,0 +1,96 @@
+<%- include('partials/header', { title: 'Biometrics' }) %>
+
+<section class="page-head">
+  <div>
+    <h1>Biometrics &amp; ID</h1>
+    <p class="subtle">Each family member's physical &amp; emergency profile — a child-ID / medical record for when it matters. Private to you.</p>
+  </div>
+</section>
+
+<section class="purchase-grid" style="grid-template-columns: repeat(auto-fill, minmax(340px, 1fr));">
+  <% profiles.forEach(p => { const d = p.derived; %>
+    <article class="purchase glass" data-person="<%= p.person_id %>">
+      <header>
+        <h3><%= p.full_name %></h3>
+        <% if (d.isChild) { %><span class="chip" style="background:rgba(83,155,245,.2)">child<%= d.age!=null?(' · '+d.age):'' %></span>
+        <% } else if (d.age!=null) { %><span class="chip" style="background:rgba(128,128,128,.15)"><%= d.age %> yrs</span><% } %>
+      </header>
+
+      <div class="amount" style="font-size:1.1rem">
+        <% if (d.bmi) { %>BMI <%= d.bmi %><% if (d.bmiCategory) { %> <span class="subtle">(<%= d.bmiCategory %>)</span><% } else if (d.isChild) { %> <span class="subtle">— <a href="<%= d.cdcPercentileUrl %>" target="_blank" rel="noopener noreferrer">CDC percentile ↗</a></span><% } %>
+        <% } else { %><span class="subtle">no height/weight yet</span><% } %>
+      </div>
+
+      <dl class="meta">
+        <% if (p.height_in) { %><dt>Height</dt><dd><%= Math.floor(p.height_in/12) %>'<%= Math.round(p.height_in%12) %>" (<%= p.height_in %> in)</dd><% } %>
+        <% if (p.weight_lb) { %><dt>Weight</dt><dd><%= p.weight_lb %> lb</dd><% } %>
+        <% if (p.blood_type) { %><dt>Blood type</dt><dd><%= p.blood_type %></dd><% } %>
+        <% if (p.eye_color || p.hair_color) { %><dt>Eyes / hair</dt><dd><%= p.eye_color||'—' %> / <%= p.hair_color||'—' %></dd><% } %>
+        <% if (p.allergies) { %><dt>Allergies</dt><dd style="color:#e5534b"><%= p.allergies %></dd><% } %>
+        <% if (p.conditions) { %><dt>Conditions</dt><dd><%= p.conditions %></dd><% } %>
+        <% if (p.distinguishing_marks) { %><dt>Marks</dt><dd><%= p.distinguishing_marks %></dd><% } %>
+        <% if (p.emergency_contact) { %><dt>Emergency</dt><dd><%= p.emergency_contact %></dd><% } %>
+        <dt>Prints on file</dt><dd><%= p.prints_on_file ? ('Yes' + (p.prints_location?(' · '+p.prints_location):'')) : 'No' %></dd>
+      </dl>
+
+      <footer class="card-foot">
+        <span class="subtle" style="font-size:.72rem">Sensitive — informational, not medical advice.</span>
+        <button type="button" class="btn tiny" data-edit="<%= p.person_id %>">Edit</button>
+      </footer>
+
+      <form class="bioForm add-form" data-form="<%= p.person_id %>" hidden style="margin-top:.6rem">
+        <div class="row">
+          <label>Date of birth<input name="date_of_birth" type="date" value="<%= p.date_of_birth ? new Date(p.date_of_birth).toISOString().slice(0,10) : '' %>"></label>
+          <label>Sex<select name="sex"><option value="">—</option><option value="m" <%= p.sex==='m'?'selected':'' %>>M</option><option value="f" <%= p.sex==='f'?'selected':'' %>>F</option><option value="x" <%= p.sex==='x'?'selected':'' %>>X</option></select></label>
+        </div>
+        <div class="row">
+          <label>Height (in)<input name="height_in" type="number" step="0.1" value="<%= p.height_in||'' %>"></label>
+          <label>Weight (lb)<input name="weight_lb" type="number" step="0.1" value="<%= p.weight_lb||'' %>"></label>
+          <label>Blood type<input name="blood_type" value="<%= p.blood_type||'' %>" placeholder="O+"></label>
+        </div>
+        <div class="row">
+          <label>Eye color<input name="eye_color" value="<%= p.eye_color||'' %>"></label>
+          <label>Hair color<input name="hair_color" value="<%= p.hair_color||'' %>"></label>
+        </div>
+        <div class="row">
+          <label class="grow">Allergies<input name="allergies" value="<%= p.allergies||'' %>"></label>
+          <label class="grow">Conditions<input name="conditions" value="<%= p.conditions||'' %>"></label>
+        </div>
+        <div class="row">
+          <label class="grow">Distinguishing marks<input name="distinguishing_marks" value="<%= p.distinguishing_marks||'' %>"></label>
+        </div>
+        <div class="row">
+          <label class="grow">Emergency contact<input name="emergency_contact" value="<%= p.emergency_contact||'' %>"></label>
+        </div>
+        <div class="row">
+          <label>Prints on file<input name="prints_on_file" type="checkbox" <%= p.prints_on_file?'checked':'' %>></label>
+          <label class="grow">Prints location<input name="prints_location" value="<%= p.prints_location||'' %>" placeholder="home safe / pediatrician"></label>
+          <button type="submit" class="btn primary">Save</button>
+        </div>
+      </form>
+    </article>
+  <% }) %>
+</section>
+
+<% if (!profiles.length) { %>
+  <section class="empty glass"><p>Add family members in <a href="/household">Household</a> first, then fill in their biometric profiles here.</p></section>
+<% } %>
+
+<script>
+  window.__csrf = '<%= csrfToken %>';
+  document.querySelectorAll('[data-edit]').forEach(b => b.addEventListener('click', () => {
+    const f = document.querySelector('[data-form="'+b.dataset.edit+'"]'); f.hidden = !f.hidden;
+  }));
+  document.querySelectorAll('.bioForm').forEach(form => form.addEventListener('submit', async (e) => {
+    e.preventDefault();
+    const pid = form.dataset.form;
+    const fd = {}; new FormData(form).forEach((v,k)=>fd[k]=v);
+    fd.prints_on_file = form.querySelector('[name=prints_on_file]').checked;
+    fd._csrf = window.__csrf;
+    const r = await fetch('/api/biometrics/'+pid, {method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(fd)});
+    const j = await r.json();
+    if (j.ok) location.reload(); else alert(j.error||'error');
+  }));
+</script>
+
+<%- include('partials/footer', {}) %>
diff --git a/views/partials/header.ejs b/views/partials/header.ejs
index 9fe26d3..7756feb 100644
--- a/views/partials/header.ejs
+++ b/views/partials/header.ejs
@@ -30,6 +30,7 @@
       <a href="/reorders">Reorders</a>
       <a href="/savings">Savings</a>
       <a href="/health">Health</a>
+      <a href="/biometrics">Biometrics</a>
       <a href="/medications">Meds</a>
       <a href="/prescriptions">Rx</a>
       <a href="/household">Household</a>

← a4f7803 auto-save: 2026-07-13T01:21:52 (4 files) — lib/ids.js db/mig  ·  back to AbramsOS  ·  docs: roadmap — health track done; Assets/Neighborhood/Biome 4c949de →