[object Object]

← back to AbramsOS

recall-watch: apply contrarian FIX-FIRST — E-known+in-date=>POSSIBLE regardless of FDA publish date (fix false-clear); assess ALL recalls per NDC (worst class never hidden); EXP-anchored expiry parser (no MFG/ref-date mis-parse); local notification on new alerts; per-NDC worst-status UI + N-of-M affected fills

4ccbda564cebd68bc41d68a769075b33a68aac8d · 2026-07-08 15:58:17 -0700 · Steve

Files touched

Diff

commit 4ccbda564cebd68bc41d68a769075b33a68aac8d
Author: Steve <steve@designerwallcoverings.com>
Date:   Wed Jul 8 15:58:17 2026 -0700

    recall-watch: apply contrarian FIX-FIRST — E-known+in-date=>POSSIBLE regardless of FDA publish date (fix false-clear); assess ALL recalls per NDC (worst class never hidden); EXP-anchored expiry parser (no MFG/ref-date mis-parse); local notification on new alerts; per-NDC worst-status UI + N-of-M affected fills
---
 data/recall-alerts.jsonl     |  4 ++
 data/recall-watch-state.json | 18 ++++-----
 routes/recalls.js            | 21 +++++++---
 scripts/recall-watch.js      | 91 +++++++++++++++++++++++++++-----------------
 views/prescriptions.ejs      |  2 +-
 views/recalls.ejs            |  8 ++--
 6 files changed, 89 insertions(+), 55 deletions(-)

diff --git a/data/recall-alerts.jsonl b/data/recall-alerts.jsonl
index d505454..b5d21c5 100644
--- a/data/recall-alerts.jsonl
+++ b/data/recall-alerts.jsonl
@@ -1,2 +1,6 @@
 {"ndc":"00032-3016-13","drug":"CREON","recall":"D-0402-2021","class":"Class II","status":"confirm","at":"2026-07-08T22:46:31.161Z"}
 {"ndc":"60505-0829-01","drug":"FLUTICASONE PROP 50 MCG SPR","recall":"D-0326-2024","class":"Class II","status":"possible","at":"2026-07-08T22:46:33.007Z"}
+{"ndc":"00032-3016-13","drug":"CREON","class":"Class II","status":"confirm","at":"2026-07-08T22:55:37.034Z"}
+{"ndc":"60505-0829-01","drug":"FLUTICASONE PROP 50 MCG SPR","class":"Class II","status":"possible","at":"2026-07-08T22:55:37.314Z"}
+{"ndc":"00032-3016-13","drug":"CREON","class":"Class II","status":"confirm","at":"2026-07-08T22:58:01.224Z"}
+{"ndc":"60505-0829-01","drug":"FLUTICASONE PROP 50 MCG SPR","class":"Class II","status":"possible","at":"2026-07-08T22:58:01.528Z"}
diff --git a/data/recall-watch-state.json b/data/recall-watch-state.json
index cd93abe..cae9826 100644
--- a/data/recall-watch-state.json
+++ b/data/recall-watch-state.json
@@ -1,25 +1,25 @@
 {
  "seen": {
-  "00032-3016-13|D-0402-2021": {
+  "00032-3016-13|Class II": {
    "status": "confirm",
    "class": "Class II",
    "drug": "CREON"
   },
-  "00121-0868-16|D-0721-2021": {
-   "status": "cleared",
-   "class": "Class II",
-   "drug": "NYSTATIN"
-  },
-  "60505-0829-01|D-0326-2024": {
+  "60505-0829-01|Class II": {
    "status": "possible",
    "class": "Class II",
    "drug": "FLUTICASONE PROP 50 MCG SPR"
   },
-  "60505-2579-08|D-0153-2020": {
+  "00121-0868-16|Class II": {
+   "status": "cleared",
+   "class": "Class II",
+   "drug": "NYSTATIN"
+  },
+  "60505-2579-08|Class III": {
    "status": "cleared",
    "class": "Class III",
    "drug": "ATORVASTATIN 20 MG TABLET"
   }
  },
- "ran_at": "2026-07-08T22:46:45.031Z"
+ "ran_at": "2026-07-08T22:58:14.009Z"
 }
\ No newline at end of file
diff --git a/routes/recalls.js b/routes/recalls.js
index 586981f..1fa8070 100644
--- a/routes/recalls.js
+++ b/routes/recalls.js
@@ -53,16 +53,25 @@ router.get('/recalls', async (req, res) => {
 
   // NDC-PRECISE: fills whose ACTUAL dispensed product (NDC) is on an FDA recall list. This is the
   // actionable signal (vs the by-name advisory above). Grouped per product, most-severe first.
+  // One row per (product, person). Status/assessment now vary per fill (a 2023 vs 2026 fill of the
+  // same drug can differ), so we aggregate to the WORST relevance and surface how many fills are
+  // affected. rk() = relevance rank (possible > confirm > cleared).
+  const rk = `(CASE pf.ndc_recall_status WHEN 'possible' THEN 3 WHEN 'confirm' THEN 2 ELSE 1 END)`;
   const ndcFlags = (await db.query(
-    `SELECT pf.drug_name, pf.ndc, pf.ndc_recall_number, pf.ndc_recall_class, pf.ndc_recall_reason,
-            coalesce(pp.full_name,'—') AS person, count(*)::int AS fills,
+    `SELECT pf.drug_name, pf.ndc, pf.ndc_recall_class, coalesce(pp.full_name,'—') AS person,
+            count(*)::int AS fills,
+            count(*) FILTER (WHERE pf.ndc_recall_status <> 'cleared')::int AS relevant_fills,
             min(pf.fill_date) AS first_fill, max(pf.fill_date) AS last_fill,
-            (array_agg(pf.ndc_recall_links ORDER BY pf.fill_date DESC))[1] AS links,
-            max(pf.ndc_recall_status) AS status, max(pf.ndc_recall_assessment) AS assessment
+            (array_agg(pf.ndc_recall_links   ORDER BY pf.fill_date DESC))[1]        AS links,
+            (array_agg(pf.ndc_recall_status  ORDER BY ${rk} DESC))[1]              AS status,
+            (array_agg(pf.ndc_recall_assessment ORDER BY ${rk} DESC))[1]           AS assessment,
+            (array_agg(pf.ndc_recall_number  ORDER BY ${rk} DESC))[1]              AS ndc_recall_number
        FROM prescription_fill pf LEFT JOIN person pp ON pp.id = pf.person_id
       WHERE pf.user_id = $1 AND pf.ndc_recalled
-      GROUP BY pf.drug_name, pf.ndc, pf.ndc_recall_number, pf.ndc_recall_class, pf.ndc_recall_reason, pp.full_name
-      ORDER BY (CASE pf.ndc_recall_class WHEN 'Class I' THEN 1 WHEN 'Class II' THEN 2 ELSE 3 END), max(pf.fill_date) DESC`,
+      GROUP BY pf.drug_name, pf.ndc, pf.ndc_recall_class, pp.full_name
+      ORDER BY max(${rk}) DESC,
+               (CASE pf.ndc_recall_class WHEN 'Class I' THEN 1 WHEN 'Class II' THEN 2 ELSE 3 END),
+               max(pf.fill_date) DESC`,
     [DEV_USER_ID]
   )).rows;
 
diff --git a/scripts/recall-watch.js b/scripts/recall-watch.js
index ea0de18..c8bb25d 100644
--- a/scripts/recall-watch.js
+++ b/scripts/recall-watch.js
@@ -31,35 +31,44 @@ const segPair = (ndc) => { const p = String(ndc || '').split('-'); return (p.len
 const candidates = (ndc) => { const p = String(ndc).split('-'); if (p.length < 2) return []; const c = new Set([`${p[0]}-${p[1]}`]); if (p[0][0] === '0') c.add(`${p[0].replace(/^0/, '')}-${p[1]}`); return [...c]; };
 const fdaDate = (d) => (d && d.length === 8 ? new Date(`${d.slice(0, 4)}-${d.slice(4, 6)}-${d.slice(6, 8)}`) : null);
 
-// parse the LATEST expiration date from recall code_info / product_description free text
+// parse the LATEST EXPIRATION date from recall free text. ANCHORED on an EXP/EXPIRE/USE-BY
+// keyword so a manufacturing date ("MFG APR 2021") or a stray "MON YYYY" is never mistaken for
+// an expiry (which would wrongly CLEAR a genuinely-recalled fill). Unanchored dates are ignored.
 const MON = { JAN: 0, FEB: 1, MAR: 2, APR: 3, MAY: 4, JUN: 5, JUL: 6, AUG: 7, SEP: 8, OCT: 9, NOV: 10, DEC: 11 };
 function parseExpiry(text) {
   const s = String(text || '').toUpperCase(); let best = null; const push = (d) => { if (d && !isNaN(d) && (!best || d > best)) best = d; };
-  let m; const re1 = /(\d{1,2})\s*([A-Z]{3})\s*(\d{4})/g;                 // 30SEP2022
-  while ((m = re1.exec(s))) if (m[2] in MON) push(new Date(Date.UTC(+m[3], MON[m[2]], +m[1])));
-  const re2 = /EXP[.:]*\s*(\d{1,2})\/(\d{4})/g;                          // Exp. 03/2022 (MM/YYYY)
-  while ((m = re2.exec(s))) push(new Date(Date.UTC(+m[2], +m[1], 0)));   // end of that month
-  const re3 = /EXP[.:]*\s*(\d{1,2})\/(\d{1,2})\/(\d{4})/g;               // 09/30/2026
-  while ((m = re3.exec(s))) push(new Date(Date.UTC(+m[3], +m[1] - 1, +m[2])));
-  const re4 = /([A-Z]{3})\s*(\d{4})/g;                                   // SEP 2022
-  while ((m = re4.exec(s))) if (m[1] in MON) push(new Date(Date.UTC(+m[2], MON[m[1]] + 1, 0)));
+  // grab the token(s) immediately after each EXP/EXPIRE/EXPIRATION/USE BY marker
+  const segs = s.match(/(?:EXPIRATION|EXPIRES?|EXPIR\w*|EXP|USE\s*BY)[.:\s]*([A-Z0-9][A-Z0-9/.\- ]{3,14})/g) || [];
+  for (const seg of segs) {
+    const v = seg.replace(/^(?:EXPIRATION|EXPIRES?|EXPIR\w*|EXP|USE\s*BY)[.:\s]*/, '').trim();
+    let m;
+    if ((m = v.match(/^(\d{1,2})\s*([A-Z]{3})\s*(\d{4})/)) && m[2] in MON) push(new Date(Date.UTC(+m[3], MON[m[2]], +m[1])));           // 30SEP2022
+    else if ((m = v.match(/^(\d{1,2})\/(\d{1,2})\/(\d{4})/))) push(new Date(Date.UTC(+m[3], +m[1] - 1, +m[2])));                       // 09/30/2026
+    else if ((m = v.match(/^(\d{1,2})\/(\d{4})/))) push(new Date(Date.UTC(+m[2], +m[1], 0)));                                          // 03/2022
+    else if ((m = v.match(/^(\d{4})-(\d{2})-(\d{2})/))) push(new Date(Date.UTC(+m[1], +m[2] - 1, +m[3])));                            // 2022-10-31
+    else if ((m = v.match(/^([A-Z]{3})\s*(\d{4})/)) && m[1] in MON) push(new Date(Date.UTC(+m[2], MON[m[1]] + 1, 0)));                 // SEP 2022
+  }
   return best;
 }
+const RANK = { possible: 3, confirm: 2, cleared: 1 };
+const worse = (a, b) => ((RANK[a] || 0) >= (RANK[b] || 0) ? a : b);
 function assess(fill, init, exp) {
   const F = new Date(fill); if (isNaN(F)) return { status: 'confirm', reason: 'no fill date' };
+  const iso = (d) => d.toISOString().slice(0, 10);
   if (exp && !isNaN(exp)) {
-    if (exp < F) return { status: 'cleared', reason: `recalled lot expired ${exp.toISOString().slice(0, 10)}, before this ${F.toISOString().slice(0, 10)} fill — different lot.` };
-    if (init && init <= new Date(+F + 183 * DAY)) return { status: 'possible', reason: `recalled lot was in-date (exp ${exp.toISOString().slice(0, 10)}) at your ${F.toISOString().slice(0, 10)} fill — confirm your lot #.` };
-    return { status: 'cleared', reason: `recall postdates this fill.` };
+    if (exp < F) return { status: 'cleared', reason: `recalled lot expired ${iso(exp)}, before this ${iso(F)} fill — a different lot.` };
+    // E >= F: that lot was still IN-DATE when this was dispensed, so it could have been in the
+    // bottle — regardless of when FDA later announced the recall (a defect found later was present
+    // when dispensed). Exposure is set by the lot window, not the FDA publish date.
+    return { status: 'possible', reason: `recalled lot was in-date (exp ${iso(exp)}) at your ${iso(F)} fill — check your lot # against the recall.` };
   }
   if (init) {
     if (init >= new Date(+F - 913 * DAY) && init <= new Date(+F + 183 * DAY))
-      return { status: 'confirm', reason: `recall (${init.toISOString().slice(0, 10)}) is near your ${F.toISOString().slice(0, 10)} fill and no lot expiry is published — confirm your lot #.` };
-    return { status: 'cleared', reason: `recall (${init.toISOString().slice(0, 10)}) is far from your ${F.toISOString().slice(0, 10)} fill.` };
+      return { status: 'confirm', reason: `recall (${iso(init)}) is near your ${iso(F)} fill and no lot expiry is published — check your lot #.` };
+    return { status: 'cleared', reason: `recall (${iso(init)}) is far from your ${iso(F)} fill.` };
   }
-  return { status: 'confirm', reason: 'recall dates unavailable — confirm your lot #.' };
+  return { status: 'confirm', reason: 'recall dates unavailable — check your lot #.' };
 }
-const worse = (a, b) => { const rank = { possible: 3, confirm: 2, cleared: 1 }; return (rank[a] || 0) >= (rank[b] || 0) ? a : b; };
 
 async function fetchRecallsForNdc(ndc) {
   const want = segPair(ndc); if (!want) return [];
@@ -96,41 +105,53 @@ async function main() {
   // reset the source's recall flags, then recompute
   await db.query(`UPDATE prescription_fill SET ndc_recalled=false, ndc_recall_number=NULL, ndc_recall_class=NULL, ndc_recall_reason=NULL, ndc_recall_status=NULL, ndc_recall_assessment=NULL WHERE user_id=$1 AND source=$2`, [USER, SOURCE]);
 
+  const clsRank = { 'Class I': 3, 'Class II': 2, 'Class III': 1 };
+  const worseClass = (a, b) => ((clsRank[a] || 0) >= (clsRank[b] || 0) ? a : b);
   for (const [ndc, ndcFills] of byNdc) {
     const recs = await fetchRecallsForNdc(ndc);
     if (!recs.length) continue;
     flaggedNdcs++;
-    // pick the recall with the latest lot expiry (most likely to overlap) + capture worst class
-    let worstClassRank = 0, chosen = null, chosenExp = null;
-    const clsRank = { 'Class I': 3, 'Class II': 2, 'Class III': 1 };
-    for (const rec of recs) {
-      const exp = parseExpiry(`${rec.code_info || ''} ${rec.product_description || ''}`);
-      const cr = clsRank[rec.classification] || 0;
-      if (!chosen || (exp && (!chosenExp || exp > chosenExp)) || cr > worstClassRank) { chosen = rec; chosenExp = exp; worstClassRank = Math.max(worstClassRank, cr); }
-    }
-    const init = fdaDate(chosen.recall_initiation_date);
+    // pre-parse EVERY matching recall (don't collapse to one — a Class I must never hide behind a Class II)
+    const parsed = recs.map((rec) => ({ rec, exp: parseExpiry(`${rec.code_info || ''} ${rec.product_description || ''}`), init: fdaDate(rec.recall_initiation_date), cls: rec.classification }));
+    const worstClass = parsed.reduce((w, p) => worseClass(w, p.cls), null);   // severity shown for the NDC (over all recalls)
     let ndcStatus = 'cleared';
     for (const f of ndcFills) {
-      const a = assess(f.fill_date, init, chosenExp);
-      ndcStatus = worse(ndcStatus, a.status);
+      // assess this fill against ALL matching recalls; keep the one with the worst relevance/class
+      let best = null;
+      for (const p of parsed) {
+        const a = assess(f.fill_date, p.init, p.exp);
+        if (!best || (RANK[a.status] || 0) > (RANK[best.status] || 0)
+          || (a.status === best.status && (clsRank[p.cls] || 0) > (clsRank[best.cls] || 0))) best = { ...a, rec: p.rec, cls: p.cls };
+      }
+      ndcStatus = worse(ndcStatus, best.status);
       await db.query(
         `UPDATE prescription_fill SET ndc_recalled=true, ndc_recall_number=$1, ndc_recall_class=$2,
            ndc_recall_reason=$3, ndc_recall_status=$4, ndc_recall_assessment=$5 WHERE id=$6`,
-        [chosen.recall_number || null, chosen.classification || null,
-         (chosen.reason_for_recall || '').slice(0, 300), a.status, a.reason, f.id]);
+        [best.rec.recall_number || null, worstClass,   // show the WORST class across all matching recalls
+         (best.rec.reason_for_recall || '').slice(0, 300), best.status, best.reason, f.id]);
       flaggedFills++;
     }
-    const key = `${ndc}|${chosen.recall_number}`;
-    seen[key] = { status: ndcStatus, class: chosen.classification, drug: ndcFills[0].drug_name };
-    // NEW alert = a recall we haven't seen for this NDC before, AND it's relevant (not cleared)
+    const key = `${ndc}|${worstClass}`;   // baseline keyed by NDC+severity (survives which single recall wins the row)
+    seen[key] = { status: ndcStatus, class: worstClass, drug: ndcFills[0].drug_name };
+    // NEW alert = an (NDC, severity) we haven't seen before AND it's date-relevant (not cleared)
     if (!prev.seen[key] && ndcStatus !== 'cleared') {
-      newAlerts.push({ ndc, drug: ndcFills[0].drug_name, recall: chosen.recall_number, class: chosen.classification, status: ndcStatus, at: new Date().toISOString() });
+      newAlerts.push({ ndc, drug: ndcFills[0].drug_name, class: worstClass, status: ndcStatus, at: new Date().toISOString() });
     }
   }
 
   fs.mkdirSync(path.dirname(STATE), { recursive: true });
   fs.writeFileSync(STATE, JSON.stringify({ seen, ran_at: new Date().toISOString() }, null, 1));
-  if (newAlerts.length) { for (const a of newAlerts) fs.appendFileSync(ALERTS, JSON.stringify(a) + '\n'); }
+  if (newAlerts.length) {
+    for (const a of newAlerts) fs.appendFileSync(ALERTS, JSON.stringify(a) + '\n');
+    // surface it — a silent nightly health check is not an alert. Local macOS notification ($0,
+    // no external send). Email/SMS would be outward-facing (gated) — left for Steve to approve.
+    try {
+      const { execFileSync } = require('child_process');
+      const drugs = [...new Set(newAlerts.map((a) => a.drug))].slice(0, 3).join(', ');
+      const body = `${newAlerts.length} new FDA recall(s) may affect your fills: ${drugs}. Open AbramsOS → Recalls.`;
+      execFileSync('osascript', ['-e', `display notification "${body.replace(/"/g, "'")}" with title "AbramsOS · Rx recall" sound name "Basso"`]);
+    } catch { /* headless / no GUI — the JSONL + audit log still captured it */ }
+  }
 
   await audit.log({ actorType: 'system', actorId: 'recall-watch', objectType: 'prescription_fill', objectId: SOURCE,
     eventType: 'recall_watch_run', metadata: { medical: true, source: 'openFDA', ndcs_recalled: flaggedNdcs, fills_flagged: flaggedFills, new_alerts: newAlerts.length } });
@@ -138,7 +159,7 @@ async function main() {
   const sum = {}; for (const k of Object.keys(seen)) { const s = seen[k].status; sum[s] = (sum[s] || 0) + 1; }
   console.log(`\nDONE: ${flaggedNdcs} NDCs on a recall list · ${flaggedFills} fills assessed`);
   console.log(`  by relevance: ${Object.entries(sum).map(([k, v]) => `${k}=${v}`).join(', ') || 'none'}`);
-  if (newAlerts.length) { console.log(`  🔔 ${newAlerts.length} NEW relevant alert(s):`); newAlerts.forEach((a) => console.log(`     ${a.drug} ${a.recall} (${a.class}, ${a.status})`)); }
+  if (newAlerts.length) { console.log(`  🔔 ${newAlerts.length} NEW relevant alert(s):`); newAlerts.forEach((a) => console.log(`     ${a.drug} — ${a.class}, ${a.status} (NDC ${a.ndc})`)); }
   else console.log('  ✓ no new relevant recalls since last run.');
   process.exit(0);
 }
diff --git a/views/prescriptions.ejs b/views/prescriptions.ejs
index 2dac895..5bd1095 100644
--- a/views/prescriptions.ejs
+++ b/views/prescriptions.ejs
@@ -46,7 +46,7 @@
             data-find="<%= ((f.drug_name||'')+' '+(f.doctor||'')+' '+(f.rx_number||'')).toLowerCase() %>">
           <td class="mono"><%= f.fill_date ? String(f.fill_date).slice(0,10) : '' %></td>
           <td><%= f.person_name || '' %></td>
-          <td><%= f.drug_name %><% if (f.ndc_recalled) { var clr = f.ndc_recall_status==='cleared' || f.ndc_recall_status==='likely_clear'; %> <span class="recall-flag <%= clr ? 'clear' : '' %>" title="<%= f.ndc_recall_class %> <%= f.ndc_recall_number %> — <%= (f.ndc_recall_assessment||'on an FDA recall list').replace(/&/g,'&amp;').replace(/\"/g,'&quot;') %>"><%= clr ? '✓ recall (lot clear)' : '⚠ RECALL' %></span><% } %></td>
+          <td><%= f.drug_name %><% if (f.ndc_recalled) { var clr = f.ndc_recall_status==='cleared'; %> <span class="recall-flag <%= clr ? 'clear' : '' %>" title="<%= f.ndc_recall_class %> <%= f.ndc_recall_number %> — <%= (f.ndc_recall_assessment||'on an FDA recall list').replace(/&/g,'&amp;').replace(/\"/g,'&quot;') %>"><%= clr ? '✓ recall (lot clear)' : (f.ndc_recall_status==='possible' ? '⚠ RECALL — check lot' : '⚠ recall — check lot') %></span><% } %></td>
           <td class="mono"><%= f.rx_number || '' %><% if (f.refill) { %><span class="subtle"> · rf<%= f.refill %></span><% } %></td>
           <td class="num"><%= f.quantity != null ? f.quantity : '' %></td>
           <td class="num"><%= f.days_supply != null ? f.days_supply : '' %></td>
diff --git a/views/recalls.ejs b/views/recalls.ejs
index f8af624..eb40c43 100644
--- a/views/recalls.ejs
+++ b/views/recalls.ejs
@@ -23,9 +23,9 @@
     <tbody>
       <% ndcF.forEach(function(x){
            var badge = x.ndc_recall_class==='Class I' ? '#f87171' : x.ndc_recall_class==='Class II' ? '#fbbf24' : '#9aa0aa';
-           var clear = x.status==='cleared', amber = x.status==='likely_clear';
-           var sBg = clear ? '#7fd0a0' : amber ? '#fbbf24' : '#f87171';
-           var sTxt = clear ? '✓ Clear' : amber ? '⚠ Confirm lot' : '⚠ Review'; %>
+           var poss = x.status==='possible', conf = x.status==='confirm';
+           var sBg = poss ? '#f87171' : conf ? '#fbbf24' : '#7fd0a0';
+           var sTxt = poss ? '⚠ Possible match' : conf ? '⚠ Check lot' : '✓ Clear'; %>
         <tr>
           <td><span class="sev" style="background:<%= sBg %>22;color:<%= sBg %>;border:1px solid <%= sBg %>66"><%= sTxt %></span></td>
           <td><span class="sev" style="background:<%= badge %>22;color:<%= badge %>;border:1px solid <%= badge %>55"><%= x.ndc_recall_class || '—' %></span></td>
@@ -33,7 +33,7 @@
           <td><%= x.person %></td>
           <td class="mono"><%= x.ndc %></td>
           <td class="mono"><a href="https://www.accessdata.fda.gov/scripts/ires/index.cfm?Product=<%= encodeURIComponent(x.ndc_recall_number||'') %>" target="_blank" rel="noopener noreferrer"><%= x.ndc_recall_number || '' %></a></td>
-          <td class="num"><%= x.fills %></td>
+          <td class="num"><% if (x.relevant_fills > 0) { %><strong style="color:#f87171"><%= x.relevant_fills %></strong> of <% } %><%= x.fills %></td>
           <td class="mono"><%= x.first_fill ? String(x.first_fill).slice(0,10) : '' %> → <%= x.last_fill ? String(x.last_fill).slice(0,10) : '' %></td>
           <td class="reason"><%= x.assessment || ((x.ndc_recall_reason||'').slice(0,110)) %></td>
           <td class="links"><% (x.links || []).forEach(function(l){ %><a href="<%= l.url %>" target="_blank" rel="noopener noreferrer"><%= l.label %></a><% }) %></td>

← 362c884 recall-watch.js: nightly-ready FDA recall watcher with PROGR  ·  back to AbramsOS  ·  chore: lint (node --check ✓), untrack runtime state, v0.2.0 1192d3c →