[object Object]

← back to AbramsOS

health: BP-medication context — show active meds that raise/lower blood pressure alongside the trend

355389e5e428270d3884139a53acf5c302950dec · 2026-07-13 01:10:57 -0700 · Steve

- lib/bp-meds.js: deterministic classifier (curated generic stems + names -> effect/class; never LLM-guessed) — ACE/ARB/beta-blocker/CCB/diuretic (lowers) + NSAID/decongestant/steroid/stimulant (raises)
- routes/vitals.js: read-only join to medication table, bpRelevant() into /health
- views/vitals.ejs: 'Medications that affect blood pressure' panel (color-coded lowers/raises), informational-not-advice
- works when med list is populated (currently empty)

Files touched

Diff

commit 355389e5e428270d3884139a53acf5c302950dec
Author: Steve <steve@designerwallcoverings.com>
Date:   Mon Jul 13 01:10:57 2026 -0700

    health: BP-medication context — show active meds that raise/lower blood pressure alongside the trend
    
    - lib/bp-meds.js: deterministic classifier (curated generic stems + names -> effect/class; never LLM-guessed) — ACE/ARB/beta-blocker/CCB/diuretic (lowers) + NSAID/decongestant/steroid/stimulant (raises)
    - routes/vitals.js: read-only join to medication table, bpRelevant() into /health
    - views/vitals.ejs: 'Medications that affect blood pressure' panel (color-coded lowers/raises), informational-not-advice
    - works when med list is populated (currently empty)
---
 lib/bp-meds.js   | 72 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 routes/vitals.js |  9 ++++++-
 views/vitals.ejs | 14 +++++++++++
 3 files changed, 94 insertions(+), 1 deletion(-)

diff --git a/lib/bp-meds.js b/lib/bp-meds.js
new file mode 100644
index 0000000..04b7a29
--- /dev/null
+++ b/lib/bp-meds.js
@@ -0,0 +1,72 @@
+// bp-meds.js — deterministic classifier: does an active medication affect blood pressure?
+//
+// Drug pharmacology is NEVER guessed by an LLM here — it's a curated map of well-known
+// generic stems / drug names to their BP effect. Matching is substring on the lowercased
+// name + generic_name. This powers the read-only "meds that affect your BP" context on
+// /health so a BP trend can be read alongside what the person actually takes.
+//
+// effect: 'lowers'  — antihypertensives (taken to bring BP down)
+//         'raises'  — can elevate BP as a side effect (NSAIDs, decongestants, stimulants)
+// This is context, NOT medical advice.
+
+// Explicit generic/brand names (highest confidence).
+const NAMED = [
+  // ACE inhibitors / ARBs
+  ['lisinopril', 'lowers', 'ACE inhibitor'], ['enalapril', 'lowers', 'ACE inhibitor'],
+  ['ramipril', 'lowers', 'ACE inhibitor'], ['benazepril', 'lowers', 'ACE inhibitor'],
+  ['losartan', 'lowers', 'ARB'], ['valsartan', 'lowers', 'ARB'], ['olmesartan', 'lowers', 'ARB'],
+  ['telmisartan', 'lowers', 'ARB'], ['irbesartan', 'lowers', 'ARB'],
+  // Beta-blockers
+  ['metoprolol', 'lowers', 'beta-blocker'], ['atenolol', 'lowers', 'beta-blocker'],
+  ['propranolol', 'lowers', 'beta-blocker'], ['carvedilol', 'lowers', 'beta-blocker'],
+  ['bisoprolol', 'lowers', 'beta-blocker'], ['labetalol', 'lowers', 'beta-blocker'], ['nadolol', 'lowers', 'beta-blocker'],
+  // Calcium channel blockers
+  ['amlodipine', 'lowers', 'calcium channel blocker'], ['nifedipine', 'lowers', 'calcium channel blocker'],
+  ['diltiazem', 'lowers', 'calcium channel blocker'], ['verapamil', 'lowers', 'calcium channel blocker'],
+  // Diuretics
+  ['hydrochlorothiazide', 'lowers', 'diuretic'], ['hctz', 'lowers', 'diuretic'],
+  ['chlorthalidone', 'lowers', 'diuretic'], ['furosemide', 'lowers', 'diuretic'],
+  ['spironolactone', 'lowers', 'diuretic'], ['triamterene', 'lowers', 'diuretic'],
+  // Other antihypertensives
+  ['clonidine', 'lowers', 'antihypertensive'], ['hydralazine', 'lowers', 'antihypertensive'],
+  ['doxazosin', 'lowers', 'alpha-blocker'], ['prazosin', 'lowers', 'alpha-blocker'], ['terazosin', 'lowers', 'alpha-blocker'],
+  // Can RAISE BP
+  ['ibuprofen', 'raises', 'NSAID'], ['naproxen', 'raises', 'NSAID'], ['celecoxib', 'raises', 'NSAID'],
+  ['indomethacin', 'raises', 'NSAID'], ['meloxicam', 'raises', 'NSAID'], ['diclofenac', 'raises', 'NSAID'], ['ketorolac', 'raises', 'NSAID'],
+  ['pseudoephedrine', 'raises', 'decongestant'], ['phenylephrine', 'raises', 'decongestant'],
+  ['prednisone', 'raises', 'corticosteroid'], ['methylprednisolone', 'raises', 'corticosteroid'],
+  ['amphetamine', 'raises', 'stimulant'], ['methylphenidate', 'raises', 'stimulant'], ['venlafaxine', 'raises', 'SNRI (dose-dependent)'],
+];
+
+// Suffix stems (catch generics not explicitly listed). Anchored to word end to limit false hits.
+const STEMS = [
+  [/\w{3,}pril\b/, 'lowers', 'ACE inhibitor'],
+  [/\w{3,}sartan\b/, 'lowers', 'ARB'],
+  [/\w{3,}olol\b/, 'lowers', 'beta-blocker'],
+  [/\w{3,}dipine\b/, 'lowers', 'calcium channel blocker'],
+];
+
+// Classify one medication → { effect, klass } or null.
+function classify(med) {
+  const hay = `${(med.name || '')} ${(med.generic_name || '')}`.toLowerCase();
+  for (const [name, effect, klass] of NAMED) {
+    if (hay.includes(name)) return { effect, klass, matched: name };
+  }
+  for (const [re, effect, klass] of STEMS) {
+    const m = hay.match(re);
+    if (m) return { effect, klass, matched: m[0] };
+  }
+  return null;
+}
+
+// Given a list of active medication rows, return only the BP-relevant ones tagged.
+function bpRelevant(meds) {
+  const out = [];
+  for (const m of meds) {
+    const c = classify(m);
+    if (c) out.push({ name: m.name, generic_name: m.generic_name, person: m.person_name || null, ...c });
+  }
+  return out;
+}
+
+module.exports = { classify, bpRelevant };
diff --git a/routes/vitals.js b/routes/vitals.js
index f1fcc56..45231cc 100644
--- a/routes/vitals.js
+++ b/routes/vitals.js
@@ -8,6 +8,7 @@ const audit = require('../lib/audit');
 const { id } = require('../lib/ids');
 const hm = require('../lib/health-metrics');
 const vitalsInsights = require('../lib/vitals-insights');
+const bpMedsLib = require('../lib/bp-meds');
 
 const router = express.Router();
 const DEV_USER_ID = 'user_steve';
@@ -56,7 +57,8 @@ router.get('/health', async (req, res) => {
     `SELECT value, measured_at FROM health_reading WHERE ${where} AND metric = '${metric}'
        ORDER BY measured_at DESC LIMIT 40`, params);
 
-  const [bp, latest, counts, weight, rhr, trends] = await Promise.all([
+  const medWhere = personId ? 'm.user_id = $1 AND m.person_id = $2' : 'm.user_id = $1';
+  const [bp, latest, counts, weight, rhr, trends, meds] = await Promise.all([
     db.query(
       `SELECT id, systolic, diastolic, category, measured_at, device_name, source
          FROM health_reading WHERE ${where} AND metric = 'blood_pressure'
@@ -69,6 +71,10 @@ router.get('/health', async (req, res) => {
     series('weight'),
     series('resting_heart_rate'),
     vitalsInsights.computeTrends(DEV_USER_ID, personId),   // cheap, DB-only (no LLM)
+    db.query(
+      `SELECT m.name, m.generic_name, p.full_name AS person_name
+         FROM medication m LEFT JOIN person p ON p.id = m.person_id
+        WHERE ${medWhere} AND m.is_active`, params),
   ]);
 
   res.render('vitals', {
@@ -80,6 +86,7 @@ router.get('/health', async (req, res) => {
     rhrSeries: rhr.rows,
     trends,
     recheck: vitalsInsights.recheckFlag(trends),
+    bpMeds: bpMedsLib.bpRelevant(meds.rows),
   });
 });
 
diff --git a/views/vitals.ejs b/views/vitals.ejs
index 8800952..485b1d3 100644
--- a/views/vitals.ejs
+++ b/views/vitals.ejs
@@ -99,6 +99,20 @@
   </section>
 <% } %>
 
+<% if (typeof bpMeds !== 'undefined' && bpMeds.length) { %>
+  <section class="glass" style="padding:.9rem 1.25rem;margin-bottom:1rem">
+    <h3 style="margin:.1rem 0 .5rem">Medications that affect blood pressure</h3>
+    <div style="display:flex;gap:.5rem;flex-wrap:wrap">
+      <% bpMeds.forEach(m => { %>
+        <span class="chip" style="background:<%= m.effect==='raises' ? 'rgba(229,83,75,.15)' : 'rgba(63,185,80,.15)' %>">
+          <strong><%= m.name %></strong> · <%= m.klass %> · <%= m.effect==='raises' ? 'may raise BP' : 'lowers BP' %><% if (m.person) { %> · <%= m.person %><% } %>
+        </span>
+      <% }) %>
+    </div>
+    <p class="subtle" style="font-size:.72rem;margin:.4rem 0 0">Context from your medication list — informational, not medical advice.</p>
+  </section>
+<% } %>
+
 <% if (latestBp) { %>
   <section class="glass" style="padding:1.25rem 1.5rem;margin-bottom:1rem">
     <div style="display:flex;align-items:baseline;gap:1rem;flex-wrap:wrap">

← 8f58fa2 health: vitals trends + insights (weight/RHR sparklines, loc  ·  back to AbramsOS  ·  health: lapsed-habit BP reading reminders (only nudges prior 8598287 →