← 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
M server.jsA views/biometrics.ejsM views/partials/header.ejs
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 & ID</h1>
+ <p class="subtle">Each family member's physical & 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 →