Select the date range for your cardiologist report:
CardiacLens logs your blood pressure, heart rate, weight, fluid intake, symptoms, medications, meals, and activities — then analyzes the patterns over time so you and your cardiologist have real evidence to work from, not just a single office reading.
Four Advanced Analytics detectors continuously watch for clinically meaningful changes: sustained heart rate shifts, decompensation warning signs, pulse pressure trends, and symptom convergence patterns. Sentinel goes further — it watches for the personal pre-event patterns that came before your own prior clinical events, not population averages. When any tool finds something significant, it tells you what was found and why it matters — in plain language.
CardiacLens is a pattern awareness tool, not a diagnostic device. It does not diagnose, prescribe, or replace your care team. All findings are described as directional — they point toward patterns worth discussing with your cardiologist, never toward a conclusion. The phrase you will see throughout the app: "Math is directional, not definitive."
CardiacLens adapts to you — your conditions, your doctor's targets, your care plan. It needs three things from you first. After that, just start logging. The app will guide you to everything else as your data builds.
Your doctor asked you to track your blood pressure — start there. Log it every time you take a reading using the Log Blood Pressure button on the main screen.
If your doctor also mentioned fluid intake, weight, or symptoms — turn those on in Settings → Tracking Features. Each one adds a button to the main screen.
Everything else can wait. The more you log, the more CardiacLens learns about your patterns — and it will let you know when a new feature is ready to be useful to you.
As your data builds, Sentinel — the status banner on your main screen — will periodically surface one suggestion at a time. Not a list of everything the app can do. Just the one thing that's most useful to you right now, based on what you've already logged. Tap the banner anytime to see it.
CardiacLens reminders run on a JavaScript timer. The screen must stay on and lit — a black screen suspends the timer. Low Power Mode must be OFF — it throttles timers even in the foreground.
Airplane Mode is fine — all logging, reminders, and monitoring work offline. Only Ask requires internet.
This is an iOS platform restriction (Safari suspends background JavaScript for installed web apps), not a CardiacLens limitation — see the FAQ for how this compares to Android.
CardiacLens can lock the entire app behind a private combination: one key letter + one color. The lock screen shows a grid of colored tiles with letters and shapes. To unlock: first tap your key letter (any color), then tap any three tiles of your chosen color. The shapes are only visual camouflage.
Set it up in Settings → 🔒 Secure Access. Takes less than a minute. See the 🔒 Security tab in Help for unlock steps, recovery, and the emergency hold-to-recover path.
Use the full setup guide below to configure everything at once — medications, daily schedule, notifications, and advanced features.
Sentinel is separate from the four detectors and works differently. Instead of watching for population-based thresholds, it watches for your personal pre-event patterns. Every time you pin a clinical event, Sentinel extracts what your data looked like in the 72 hours before it — your Footprint. When your live data resembles a prior Footprint, Sentinel tells you which event it matches and how. Tap 🛡️ Sentinel on the main screen to open it. See the 🛡️ Sentinel tab in this guide for the full explanation.
Analyzes your full HR history to find level shifts — periods where your resting heart rate durably changed to a new baseline — and rapid changes — sudden single-session jumps. Requires 6+ days of HR readings. Use the Today / 7 Days / 30 Days / All Time buttons to narrow the window. A "🔍 Investigate" button on any finding opens Anchor Date Investigation for that exact date.
Monitors five clinical signals simultaneously: weight gain, cardiac output (HR + pulse pressure), symptom frequency, fluid intake patterns, and notes keyword scan. Each signal is scored individually. The composite result ranges from ✅ All Clear → 📋 Early Indicator (1 signal) → ⚠️ Monitor Closely (2+ signals) → 🚨 Contact Cardiologist (multiple red signals). One signal alone is not cause for alarm — the value is in seeing multiple signals converge.
Pulse pressure is systolic minus diastolic BP. Normal range is 40–60 mmHg. A narrowing trend can indicate the heart is working harder to maintain output; a widening trend may indicate arterial stiffness. Many cardiac patients have a chronically reduced PP that is their personal baseline — the detector recognizes this and labels it "Chronically Narrow — Your Stable Baseline" rather than an acute alert. What matters is change from your baseline, not comparison to a textbook number.
Analyzes 30 days of symptom logs across three layers: (1) Intraday clusters — 3+ symptoms within a 4-hour window on the same day; (2) Multi-day escalation — the same symptom worsening in severity across multiple days; (3) Clinical triads — recognized symptom combinations with known cardiac significance (e.g., shortness of breath + fatigue + edema). The 60-day Timeline tab shows where clusters occurred visually.
When any detector finds a significant event (yellow or red), it suggests pinning it. Pinned events are saved permanently — they never roll off with the 90-day data window. Each pin captures the detector's full finding plus all raw log data for that date.
Pinned events appear in three places: the 📌 Pinned Events button in Analytics, the 60-day Timeline as amber markers, and your Doctor's Report as a dedicated section.
After your cardiologist appointment: use "Archive & Clear" to permanently save the current pins and start fresh for the next period. An appointment nudge reminds you automatically.
All four detectors use statistical pattern detection on self-reported data. The math is directional, not definitive. A finding means: "this pattern is present in your data and worth discussing." It does not mean: "something is wrong." Findings can reflect real clinical changes, data entry errors, device variability, or normal day-to-day fluctuation. Your cardiologist provides the clinical interpretation — CardiacLens provides the data and the pattern recognition to make that conversation more productive.
CardiacLens includes built-in safety checks that can block a medication dose before it is logged. These checks run automatically every time you tap Save in Log Medicine.
If a medicine has a condition check set, a blood pressure reading within the last 4 hours is required before the dose can be logged. If no recent reading exists, a block screen appears with a Log BP Now button. After logging BP, return to Log Medicine — your previous selection is automatically restored.
If your current BP or heart rate reading fails a medicine's condition threshold, the dose is blocked. The block screen shows the medicine name, what the condition requires, your actual current reading, and all three vitals against your target ranges. Use the Update Condition button if the threshold needs adjusting, or tap Back to return to Log Medicine.
Each medicine has a minimum time between doses based on its schedule — twice daily means 6 hours minimum, once daily means 12 hours, every 2 days means 48 hours. If your last logged dose was too recent, the block screen shows exactly when you last took it and how long you must wait. Never double dose — if you missed a dose and the next one is close, skip and resume your normal schedule.
Formula-generated MIP, pulse-pressure, and heart-rate caution screens no longer run when logging medicine. Log Medicine shows physician-defined Doctor Rules only. Today's Awareness and Personal Pattern remain observational and do not tell the user to change or skip a dose.
Open Manage Medicines, tap Add or Edit on a medicine, then enter the clinician's exact instruction in Doctor Rule / Instructions. Examples: "Hold if systolic <100", "Take with food", "Morning only", or "Split tablet." CardiacLens displays this text for awareness only; it does not use old condition rules to block or warn when logging medicine.
For medications taken every other day or on a similar pattern, select Every X days (custom) in the frequency dropdown and enter the number of days. Every other day = 2. The minimum interval block uses this number directly — a medicine set to every 2 days requires 48 hours between logged doses.
When you edit a medicine and change its name, CardiacLens asks what changed. Choose Correcting a typo to update all history to the new name. Choose New dose or formulation to keep history under the old name, mark the old medicine as Discontinued today, and start tracking the new name from today. This preserves your before/after history for clinical analysis.
If a red banner appears at the top of the app saying safety checks are disabled, a data integrity problem was detected. This may be caused by a storage error or data edited outside the app.
To restore data integrity: delete any BP readings or medications logged today and re-enter them through the app. Timing and duplicate-dose protection re-enable automatically on the next save. If the banner keeps appearing, export your backup and contact support at robert@cardiaclens.com.
Events Tile Queue: Today's Status now has one color-changing Events tile. Green means clear/next event, blue means due now, and red means missed. Tap the tile to open the full queue. The home page no longer shows large event cards, and no pop-up modal opens automatically over active logging. A soft tone plays only once when a new event becomes due while CardiacLens is already open.
Medication Intelligence now shows only three observational sections for each active medicine: Doctor Rule, Today's Awareness, and Personal Pattern.
The old calculated threshold review display and old medicine condition-rule engine have been retired. CardiacLens no longer shows 7-day mean, standard deviation, formula-generated block/warn suggestions, approved threshold boxes, pulse-pressure threshold setup, or condition-rule blocks/warnings inside Medication Intelligence or Log Medicine.
Shows physician-defined instructions only, such as “Hold if systolic <100 mmHg,” “Take with food,” “Split tablet,” or “Morning only.” If no doctor rule exists, this section is not shown.
Shows only information true today, capped at three items. Examples include hydration below goal, extreme heat today, or recent dizziness/weakness logged today. If nothing applies, it says “No special awareness items today.”
Shows one observational sentence from your own history. It uses “observed” language and does not state that a medicine caused an outcome.
Medication Intelligence no longer displays standard deviation, z-score, variance, percentiles, regression, machine-learning terminology, calculated block/warn threshold suggestions, approval controls for formula-generated thresholds, or old Condition Rules. Log Medicine no longer uses conditionRules, legacy conditionMetric values, MIP-approved thresholds, PP warnings, or HR formula warnings to block or warn medication logging.
This tab tracks bugs that were reported and fixed — things that were broken and are now resolved. Each entry describes the defective behavior, the root cause, and the fix applied.
For new features and capabilities, see the Planned Features tab.
Medication Intelligence deploy cleanup: retired medication condition rules remain inactive. Log Medicine does not use old conditionRules, MIP-approved thresholds, pulse-pressure warnings, or heart-rate formula warnings to block or warn. The user-entered Doctor Rule text is displayed for awareness only.
Live Rain Radar (Windy) now opens Windy using the direct radar URL format: https://www.windy.com/-Weather-radar-radar?radar,{latitude},{longitude},9. CardiacLens uses current GPS when allowed and falls back to the saved Weather ZIP coordinates. Open-Meteo remains the app forecast source.
conditionRules, legacy conditionMetric fields, MIP-approved thresholds, pulse-pressure warnings, and heart-rate formula warnings are not used to block or warn medication logging.Several reports over the life of this project have come down to the same root cause: iOS Safari suspends all background JavaScript for installed web apps, so reminders stop firing when the screen is off or Low Power Mode is on. Android's Chrome extends background service workers and push notifications to installed web apps; Apple does not extend the equivalent capability to web apps — only to apps distributed through the App Store, on which Apple takes a commission. CardiacLens could match Android's reliability with a server-based push architecture, but that would require an account system and a server, breaking the on-device-only, no-account privacy model. We've chosen privacy over reminder reliability on iOS. See the FAQ for the full explanation and the workaround (screen on, Low Power Mode off).
cardiaclens-fluid-push), so alerts arrive even when the app is closed or the phone is locked — not only while the app is open and on screen. Works from three sources, alone or combined: (1) automatic pacing (Settings → daily fluid target, start time, end time, no events required), (2) Scheduled Events with a fluid amount set, (3) Plan My Day entries with a fluid amount set. When more than one source has an upcoming fluid time, whichever is soonest wins. Events with no fluid amount are ignored entirely and never interact with this system. See the FAQ entry “How do fluid push reminders work” for the full breakdown by setup.sw.js?v=9.10.268). iOS Safari rejected this with a hard error: "Cannot update a service worker with a requested script URL whose newest worker has a different script URL." Reverted to ./sw.js and instead added updateViaCache: 'none' to the registration options. This is the correct iOS-compatible approach — it instructs the browser to always bypass HTTP cache when fetching sw.js to check for updates, regardless of the cache headers GitHub Pages sends. The script URL stays the same so iOS accepts it, but the update check always hits the network.sw.js with long cache headers, so the browser never re-fetched it and never discovered new versions. Users continued running stale cached HTML indefinitely. Fixed: the service worker is now registered as ./sw.js?v=9.10.268. The browser treats a changed query string as a new URL and always bypasses HTTP cache to fetch it. Every future version bump changes the query string, guaranteeing the new service worker is fetched and installed immediately on next app open. No user action ever required.cache: 'no-store', bypassing the CDN and browser cache entirely. Every app open now checks for a fresh copy of the HTML. If the network is unavailable, it falls back to the cached version. All other assets (icons, manifest) remain cache-first for performance. The new cache name cardiaclens-v9.10.267 also forces the old worker to be replaced on first load.window._clCurrentGrid is now explicitly nulled before calling _clResetKbState() in both _clShowLock and _clLockFromBtn, ensuring a fully fresh keyboard is built every time the lock screen opens. (3) Setup screen spacing: reduced tagline margin, card padding, and step-bar margin so the 3-step setup flow fits on small iPhone screens without clipping or awkward vertical centering; container now uses align-items:flex-start with margin:auto on the inner wrapper so tall content scrolls cleanly instead of fighting the centered layout.audioContext.resume() in unlockAudioContext now has a .catch() — iOS can reject this call; unhandled rejection was going to Guard Dog. (5) speechSynthesis.speak() in _speechFallback now wrapped in try/catch — iOS blocks speech synthesis without a user gesture; the resulting exception was the "Failed to start the audio device" report in Guard Dog. Also added .catch() to Notification.requestPermission() — some iOS versions reject instead of resolving with "denied".env(safe-area-inset-bottom) so it sits above the iPhone home indicator instead of behind it. (2) PIN input now uses autocomplete="new-password" to suppress iOS keychain suggestions that would appear for type="password" fields. (3) Tapping anywhere on the PIN box now focuses the input — works around iOS Safari's restriction on programmatic focus calls that aren't triggered by direct user gesture.color:#f0f4f8 — the exact same color as the background — making every character typed invisible. Combined with caret-color:transparent hiding the cursor, the input appeared completely unresponsive even though it was accepting input correctly. Users had no way to know anything was being entered. Fixed: text is now visible in navy blue at 28px, cursor is visible, placeholder says "Type your key letter". Also fixed: autocapitalize="characters" so mobile keyboards default to uppercase, matching how keys are stored.cardiaclens-ask.sommers-texan.workers.dev (which does not exist) instead of clinbridge-ask.sommers-texan.workers.dev (the actual deployed worker). The URL was incorrectly updated during the June 13 rebrand. Fixed..toLowerCase() only, without stripping whitespace. Fix: a _normMedName() helper now normalizes both sides — lowercase + collapse all internal whitespace — before comparing. Affects all three medication categories (scheduled, PRN, periodic). Any medication whose logged name has inconsistent spacing relative to the medicine list entry will now match correctly.linkedTo field set (e.g. Metoprolol 12.5mg, the current dose). It should only appear on discontinued medications with a link (e.g. Metoprolol 25mg, the superseded dose). Fixed: isLinkedPredecessor now requires both linkedTo set AND status === 'discontinued'.expectedDoses is now computed from med.startDate and med.discDate (both already stored) — only days within the medication’s active window inside the analysis period are counted. Missed-dose detection likewise skips dates outside the active window. Stopped medications show a red “Stopped [date]” badge; mid-period starts show a green “Started [date]” badge; dose-link predecessors (linkedTo set) show a purple “Prior dose” badge and are excluded from the Overall Daily Med Adherence total to avoid double-counting the same drug. No data structure changes — uses fields already present on every medication record.CL_GD_HEARTBEAT_verified key written exclusively by the real health check, allowing “On patrol…” and the real time to be distinguished from each other unambiguously. Staleness threshold increased from 10 to 30 minutes to account for iOS Low Power Mode timer suspension. “Guard Dog fell asleep” now only appears when it genuinely means something.alert() dialog whose Close button did not resume app execution on iOS. Replaced with _bpShowError() inline red banner above Save button.CARDIACLENS_MORNING_CHECKIN. Any check-in logged after the June 13 rebrand was silently overwritten by the longer ClinBridge history. Fixed: migration now performs a date-keyed union — every entry from both sides is preserved, CardiacLens (newer) record wins same-date conflicts. Also: FLUID_EVENTS_SNAPSHOT conflict now picks the side with the newer savedAt date.clinbridge_* and CLINBRIDGE_* localStorage keys. After the rebrand, the app read exclusively from cardiaclens_* / CARDIACLENS_* keys — so pinned events, breathing sessions, saved meals, appointment notes, sentinel events, Ask history, Med Intel, baseline conditions, timeline snapshots, anchor investigations, and morning check-in history were all silently invisible. Root cause: no migration was written at rebrand time. Fix: a one-time migration block runs at the top of block 1 (earliest script execution) on every device. It checks for a CL_REBRAND_MIG_V1 guard key — if absent, it copies every unmigrated clinbridge_*/CLINBRIDGE_* key to its cardiaclens_*/CARDIACLENS_* counterpart. Also merges the legacy CB_PINNED_EVENTS key into cardiaclens_pinned_events, deduplicating by source+eventDate. Guard key set once; migration never re-runs.CARDIACLENS_MORNING_CHECKIN. Any check-in logged after the June 13 rebrand (stored in the CardiacLens key) was silently overwritten by the longer ClinBridge history, making the prompt revert from “Edit” back to “Start →”. Fixed: migration now performs a date-keyed union of both arrays — every entry from both sides is preserved, with the CardiacLens (newer) record winning when the same date appears in both. Also: FLUID_EVENTS_SNAPSHOT conflict now correctly picks the side with the newer savedAt date rather than defaulting to ClinBridge.clinbridge_* and CLINBRIDGE_* localStorage keys. After the rebrand, the app read exclusively from cardiaclens_* / CARDIACLENS_* keys — so pinned events, breathing sessions, saved meals, appointment notes, sentinel events, Ask history, Med Intel, baseline conditions, timeline snapshots, anchor investigations, and morning check-in history were all silently invisible. Root cause: no migration was written at rebrand time. Fix: a one-time migration block runs at the top of block 1 (earliest script execution) on every device. It checks for a CL_REBRAND_MIG_V1 guard key — if absent, it copies every unmigrated clinbridge_*/CLINBRIDGE_* key to its cardiaclens_*/CARDIACLENS_* counterpart. Also merges the legacy CB_PINNED_EVENTS key into cardiaclens_pinned_events, deduplicating by source+eventDate. Guard key set once; migration never re-runs.analyzeMeal() (the "Evaluate" button) builds mbPendingMeal from the current items but never recorded which saved meal — if any — was being edited (mbEditingId). After the AI analysis returned, tapping "💾 Save to Library" in mbSaveLastMeal() always pushed a brand-new entry with id: Date.now(), regardless of whether the user had arrived there via "✏️ Edit" on an existing meal. The original meal was left unchanged with its old ingredients, while the edited version landed in a new, easy-to-miss duplicate entry — reported as "lost my new ingredients." Separately, "✏️ Modify & Re-analyze" (mbModifyLastMeal → _loadPendingMealIntoBuilder) called openMealBuilder() without the 'edit' flag, which explicitly resets mbEditingId to null — so even a second round of edit → evaluate → modify lost the edit context. Fixed by carrying editingId: mbEditingId through mbPendingMeal: mbSaveLastMeal() now updates the original entry in place (preserving its id and saved date, updating items/totals/lastEvaluated) when an editingId is present, and only falls back to creating a new entry for meals built from scratch; _loadPendingMealIntoBuilder() restores mbEditingId after openMealBuilder() and relabels the Save button "💾 Update" accordingly.mailto: link itself worked correctly, but in iOS Mail, the "Device & OS" and "Anything else on your mind" prompts (and the trailing 📷 emoji) collapsed onto a single line while every other question kept its blank-line separation — despite the source template using identical \n\n\n spacing throughout. Rather than rely on blank-line spacing that can collapse unpredictably, the template now numbers each prompt (1–5) with single blank lines between them, so every question stays visually distinct even if spacing collapses somewhere. The screenshot reminder is now its own labeled line ("📷 Screenshot: ...") rather than trailing into the previous question._betaReportNow() path that opens the same guided email template immediately, with zero effect on the skip counter or next check-in date — the weekly rhythm continues unaffected. Two entry points: (1) the purple "BETA BUILD" banner at the top of every screen is now tappable, with updated text "tap to report something"; (2) a new "📨 Report Something Now" button in Settings → Beta Tester, above the existing preview button.robert@cardiaclens.com with guided prompts — what you were doing, what you expected, what happened, device/OS, plus a reminder to attach a screenshot) and Nothing to Report (a valid response — just confirms you're still using it). Either response resets the skip counter and reschedules for another week, indefinitely — active testers never see anything else. Dismissing without responding ("Remind me later") counts as a skip; two consecutive skips immediately show a graduation overlay thanking the tester and linking to cardiaclens.com, with no further prompts. Since /beta and the main site share the same browser storage, graduating requires no data export or migration — it's the same data, just a different URL going forward. New Settings section (Beta Tester, /beta only) shows current week number, skip count, and next check-in date, plus a "Preview Check-In Now" button that triggers the real prompt immediately for testing — any response counts as the actual weekly check-in. New functions: _isBetaBuild(), _betaCheckInMaybeShow(), _betaCheckInRespond(), _betaCheckInSkip(), _betaFeedbackMailto().setHelpSection() hides inactive sections using six properties (display, visibility, height, overflow, padding, margin), all !important — but _helpShowAllSections() and helpGoToResult(), used by the search feature, only ever toggled display. When a search result was tapped, the target section's display was restored to block, but its leftover visibility:hidden, height:0, and overflow:hidden from a prior tab switch were never cleared — so the section was technically "shown" but visually collapsed to zero height. This also explained why Prev/Next did nothing: the highlighted <mark> elements existed and the counter was correct, but scrollIntoView() had nothing visible to scroll to inside a zero-height container. Fixed with a new shared helper, _helpSetSectionVisibility(el, show), that sets all six properties identically everywhere a section's visibility is toggled — setHelpSection, _helpShowAllSections, and helpGoToResult now all call the same function._sentDailySeries(), _sentSparklineSVG(), _sentTrendCaption(), plus lookup tables SENTINEL_METRIC_FN and SENTINEL_GOOD_DIR. No new dependencies — the sparkline is hand-built inline SVG._mipFilterMeds(), _mipSetupScroll(), _mipScrollTop().doctorReportSection panel have been removed. "🔍 Generate Analysis" and "🚚 Doctor's Report" now sit side by side under the date range selector — either can be tapped directly, in any order, and both use the same selected date range (or the last 30 days if none is selected).mipApprove() / mipApprovePP() tap rebuilt the entire panel and reset the scroll position to the very top (showModal() hardcodes scrollTop = 0), so reviewing several medicines in one sitting meant scrolling all the way back down after every single tap. Fixed with a new _mipPreserveScroll() helper: each medicine card and per-metric block now has a stable id; after the panel rebuilds, the same card/block is located again and the scroll position is restored relative to it — an exact element-anchored restore, not an estimate.history array on that medicine/metric, capped at the last 12 entries. Nothing currently displays this yet; it's intended to feed Doctor's Report trend lines in a future version (e.g. "this threshold has held steady for 6 weeks" or "this threshold moved from X to Y over the past month")._mipRangeText() helper; purely additive display, no changes to threshold calculations._mipWarnProceed() now captures vitals-at-dose and a plain-language description of what triggered (new _warnDetailSummary()) into entry.warnProceed on the medLog entry — distinct from entry.override, since this is a caution acknowledgement, not a hard-block bypass. The existing 0–120 minute post-dose BP/HR correlation now runs on these too, surfaced in amber (vs. the override section's stronger styling) in three places: Medications log entries (today's log and history), Med Effectiveness ("Caution Acknowledgements" section per medicine), and Doctor's Report ("Caution Acknowledgement Log"). Ask context gains a parallel "MEDICATION CAUTION ACKNOWLEDGEMENTS TODAY" section so Ask can reason about these patterns too. This is a beta-only change — please log a few doses past warn screens and confirm the caution acknowledgement appears correctly with vitals and (after a follow-up BP reading) the post-dose correlation, before this goes to the live site..modal-input had no explicit line-height, so the cursor used the browser's default line-height for the field's font size rather than one sized to match the text. Fixed by setting line-height:1.3 on .modal-input, which affects all modal text/number fields app-wide.type="number" with no sanitizer. iOS voice dictation writes its transcription directly into the field's value as it goes; for type="number" fields, the browser silently rejects (clears to empty, or truncates to the last valid number) any intermediate value that isn't a complete valid number — so a transcription that briefly passes through an invalid state (or includes a stray "e") can lose the decimal portion or leave "e" behind. Fixed: the Log Weight field (and the two weight-edit fields in Manage Weight History) are now type="text" inputmode="decimal", which still shows the numeric keypad but lets dictation write its full text without browser interference. A new _sanitizeDecimalInput() runs on onblur and strips any stray non-digit, non-decimal-point characters (such as "e") while preserving the decimal point and digits, so "180.2" is saved correctly.cardiaclens.com/beta shares the same browser storage (your health data) as the main site, because they're the same website (just a different folder) — back up your data before testing a beta build, same as before any update.setTimeout(renderXChart, 100)) would retry forever with no fallback ever shown. Fixed: all 4 chart functions (BP, Fluid, HR, Anchor) now cap retries at 30 attempts (3 seconds); if Chart.js still hasn't loaded by then, BP Trends shows "Chart could not load. Pull down to refresh, or check your connection." instead of an indefinite silent retry, and the other 3 charts simply stay hidden without crashing. Headless Chromium does not perfectly replicate iOS Safari/WebKit — it cannot catch iOS-specific dictation or PWA-caching behavior — but it does catch the entire class of "uncaught error silently breaks app initialization" bugs, which is what caused the v9.10.198 and v9.10.201 incidents.<script defer>, which by the HTML specification executes only after the entire document has finished parsing. The script that calls _startApp() (which in turn calls renderBPChart(), renderFluidChart(), createHRChart(), and buildAnchorCharts()) is a plain non-deferred <script> near the end of the document, which executes immediately as the parser reaches it — before the deferred Chart.js script has run. At that moment window.Chart is undefined, so new Chart(...) throws a ReferenceError. For BP Trends specifically, this happens after the "Need at least 2 readings" placeholder has already been hidden and the canvas already shown, so the result is a blank canvas with no message and no chart — matching what was reported. This explanation is the most likely mechanism based on the code structure, but has not been confirmed by reproducing the error in an actual browser console, since this environment cannot run the app live. Fixed defensively regardless: all 4 chart-creation functions (renderBPChart, renderFluidChart, createHRChart, buildAnchorCharts) now check typeof Chart==='undefined' at entry; if Chart.js hasn't executed yet, the function retries itself after 100ms instead of throwing. This is a no-op if Chart.js is already available, and self-heals the blank-chart condition if it isn't. If charts are still blank after this update, that would indicate a different cause and should be reported with the exact section affected.renderFAQ() and isn't part of the static section sweep — was independently confirmed updated. No other tab required changes. This sweep, including which tabs matched and which didn't, is now a required part of the R14 pre-zip audit output for every future release.oninput="_sanitizeNumericInput(this)", which fires on every keystroke and on every partial update during active iOS voice dictation. Synchronously rewriting an input's .value while iOS dictation is mid-session desyncs the field from the dictation engine, which can stop dictation from committing any text at all. When that happens the field is left empty, so any "Please enter a value" validation correctly (but frustratingly) keeps re-firing — the field really is empty, but only because dictation never landed. This affected all 72 numeric fields that received the v9.10.197/198 fix, including BP, HR, breathing logger, fluid amounts, durations, thresholds, and history filter ranges. Fixed: the sanitizer now runs on onblur (when you tap away from the field) instead of oninput. This lets dictation complete normally and commit its full text, then cleans up any stray non-digit characters once you're done with the field — preserving the original fix's goal (no stray "e" in saved BP/HR values) without interfering with the dictation session itself. The 12 history filter range fields (which also call a live filter-update function on every keystroke) keep that live oninput behavior unchanged and gained a separate onblur sanitizer.oninput="_sanitizeNumericInput(this)" onto numeric fields in v9.10.197/198 had a flaw when the target field was built across a JS string concatenation containing a ternary expression. In those cases (bpFluid, fluidAmount, mealFluid, flwMinA), the regex injected the attribute outside the closing quote of the string literal and inside the ternary — producing broken JS like (presetFluid oninput="...">0?... instead of valid HTML. This is a silent failure: the HTML renders visually correct but the JS syntax error prevents the entire script block from executing, so the app init function never fires. Fixed: all 4 broken injections corrected manually; JS syntax verified clean with node --check. New rules R7/R7a added to the deploy checklist requiring a mandatory JS syntax check after any automated HTML patching, and R14 updated to include this check in the pre-zip audit.pageshow BFCache event, not a fresh page load. The catch-up scan was only wired into cold-start initialization and the visibilitychange handler — not the pageshow restore path, which is the most common way the app is reopened on iPhone. Fixed: catch-up scan now also runs 800ms after a BFCache pageshow restore.keydown blocker correctly filtered keyboard entry of e/E on type="number" fields, but iOS voice dictation bypasses the keyboard entirely — it writes the transcribed text directly into the field value, never firing a keydown event. Fixed: a new _sanitizeNumericInput() function strips any non-digit character on every oninput event, covering voice, paste, autofill, and all non-keyboard input paths. Applied to all 59 integer numeric fields across the app. The keydown blocker was also extended to cover inputmode="decimal" fields (fluid and weight filter ranges, med fluid entry) which block e/E/+/- via keyboard while still allowing the decimal point. Free-text fields (meal descriptions, notes, portions, serving sizes) are unaffected and continue to accept full voice dictation including phrases like "1/4 cup" or "8 oz".checkEventReminders function filtered out any settings.dailyEvents entry whose reminderEnabled property was undefined or absent (falsy check), rather than only skipping entries explicitly set to false. Events created via certain code paths did not persist a reminderEnabled:true field, causing them to be silently skipped on every poll. Fixed: guard changed from !evt.reminderEnabled to evt.reminderEnabled===false so only deliberately disabled events are excluded. Additionally wrapped the entire function in try/catch so a runtime error cannot silently stop future ticks.\U0001f4ca instead of the 📊 emoji. Fixed: replaced the malformed escape with the emoji character directly.grid-template-columns:1fr 1fr 1fr 1fr), causing the columns to be too narrow on phone screens and overflowing the panel width. Changed to a 2×2 grid (grid-template-columns:1fr 1fr) matching the fix already applied to Meal Builder in v9.10.160.aboveCount field is included in the return object for display and future logging use.conditionRules array, meaning the rule engine evaluated those medicines as having no conditions — only the separate MIP evaluation path applied. On a reading where the rule engine passes but MIP fires, the medicine received a warn from the MIP path while other medicines with synced rules showed nothing — producing inconsistent and confusing warn behavior. Fixed with a one-time migration at app load (guarded by cardiaclens_mip_sync_v189_done flag): for every active medicine, any MIP metric (systolic, diastolic, HR, PP) that has an approved threshold but no corresponding conditionRule is upserted as a block rule and warn rule into conditionRules, matching the behavior of a freshly approved threshold. The migration only adds rules — it never modifies or removes rules the user set manually. conditionNote is regenerated after sync. Affected medicines in this backup: Eliquis 5mg (all 4 metrics missing), Farxiga 10mg (HR, PP missing), Mounjaro 2.5ml (HR, PP missing), Trazodone 100mg (HR, PP missing).mipApprove() now upserts a block rule at the block threshold and a warn rule at the warn threshold directly into the medicine's conditionRules array. If a rule for that metric already exists, its threshold is updated in-place while preserving the user-set outcome (block or warn). All MIP-synced rules are labelled MIP-approved (date) so they are distinguishable from manually entered rules. MIP-approved PP thresholds now also sync a PP warn rule across all active medicines that do not already have a user-defined PP rule. The conditionNote on each medicine is regenerated after sync so the MIP panel, Manage Medicines, and the log-time safety check all reflect the same state. Help & About updated: the "You Approve Everything" section and the Condition Rules section now accurately describe the auto-sync behavior._mipWarnProceed() set the proceed-flag to true then called saveMedicine(), but saveMedicine() immediately reset the flag to false because the sentinel argument 'warned' was never actually passed. Result: the same warn screen appeared again and again — logging could never complete after any warn acknowledgement. Fixed: _mipWarnProceed() now correctly passes 'warned' to saveMedicine(). Additionally, warn acknowledgement is now tracked per-medicine per save pass using _mipWarnAckedMeds — each medicine in a multi-selection gets its own warn screen shown exactly once, and no warn is silently bypassed when multiple medicines are logged together. Affects all users logging multiple medicines simultaneously with any warn condition active.<div> on one entry, leaving a stale </div> that closed a parent container early. This broke the DOM structure of every section after it — JavaScript’s setHelpSection hide/show calls could not reach the displaced content. Present since v9.10.32. Fixed by inserting the missing wrapper div. All 18 help sections verified balanced by automated div audit before release.tabScrollRight) was position:absolute, z-index:10, spanning full height of the tab bar, always visible (display:flex). On iPhone it permanently covered the first 52px from the right edge of the tab bar, intercepting all touch events on Getting Started, How to Use, and other nearby tabs. Fixed: overlay div set to pointer-events:none so touch passes through to tabs beneath; the arrow span inside gets pointer-events:auto so scrolling still works when the arrow itself is tapped.-webkit-overflow-scrolling:touch, display:none alone does not fully prevent hidden sections from painting. Fixed: visibility:hidden, height:0, overflow:hidden, and zeroed padding/margin added alongside display:none in setHelpSection for non-matching sections.planned-features to render inside known-issues rather than as a sibling section. Fixed: section now correctly placed as a sibling.overflow:hidden on .help-modal-content inside the #aboutModal override — reintroducing the same iOS touch-blocking condition it was trying to fix. Changed to overflow:visible. Also added touch-action:manipulation to both scroll chevron overlay divs which were positioned absolutely above the tab bar and intercepting tap events on iOS even when hidden..modal CSS class sets overflow:hidden on the backdrop element. On iOS Safari, overflow:hidden on a parent intercepts all touch events, preventing child elements from receiving scroll or tap gestures regardless of their own overflow settings. Fixed with a dedicated #aboutModal CSS rule overriding to overflow-y:auto with -webkit-overflow-scrolling:touch. Same property added to .modal-body and .hr-tabs via the #aboutModal rule block. Inline styles on both elements also updated to be consistent.openHelpModal rewrites the tab bar style.cssText via a setTimeout, which wiped out the CSS-applied -webkit-overflow-scrolling:touch rule. Without this property on iOS, touch momentum scrolling was broken — tabs could not be swiped and content could not scroll. Fixed: -webkit-overflow-scrolling:touch!important added to the inline cssText override. Also corrected padding-right to 44px so tabs can scroll past the right chevron overlay.htab-med-intel button was added to the JavaScript tabKeys arrays and the section HTML was created, but the actual <button> element was never inserted into the HTML tab bar. The tab was unreachable by tapping. Fixed: button inserted before the Deep Dive tab.welcome, tab bar scrollLeft is explicitly reset to 0 so the first tabs are always visible on open.CARDIACLENS_MED_INTEL. Approved thresholds survive data imports and are reloaded automatically._confirmImport() reloaded all other data but never reloaded the savedMeals in-memory array from localStorage. The stale pre-import array remained active, and any subsequent meal save would overwrite localStorage with that stale array, silently erasing meals that were in the backup. Fixed: savedMeals is now explicitly reloaded from localStorage immediately after all other post-import reloads.mbItems was cleared but the mbMealName and mbMealType DOM fields were never reset. Previous session's meal name and type remained visible. Fixed: both fields are now explicitly cleared in the fresh-start branch of openMealBuilder.askThread was set to display:none. The v9.10.160 fix hid the tool panels on reopen but did not restore askThread to visible — leaving Ask blank with no input response. Fixed: openAskPanel now explicitly restores askThread to display:flex alongside resetting the tool panels.grid-template-columns: 1fr 1fr 1fr 1fr), causing the fields to be too narrow to use on a phone screen. Changed to a 2×2 grid (grid-template-columns: 1fr 1fr) so all four fields are legible and tappable.settings.medicineList (active medicines only), then appends any historical medicine names from log entries not already in the list. Discontinued medicines are excluded from the dropdown.conditionMetric (systolic / diastolic / hr / null), conditionThreshold (number or null), conditionDirection (above / below / null). A new “Condition Check” section now appears at the bottom of the Add/Edit medicine screen — select Diastolic, Systolic, or Heart Rate, enter a threshold, and choose whether to block when the reading is below or above that threshold. Selecting None clears all condition fields. Existing medicines without these fields are unaffected.conditionNote (e.g. “Only when diastolic >70” or “Only when systolic >100”) now display an amber warning banner directly under the schedule note in the Log Medicine modal. Previously the condition note only appeared in Manage Medicines, meaning it was invisible at the critical moment of deciding whether to take the medication. Affects Metoprolol, Losartan, and any medicine with a clinical condition attached. Medicines whose conditionNote is simply “As needed” are unaffected since the schedule line already conveys this.hideModal() only closes #modal, tapping the FAB on those screens did nothing while covering their real close button. Fix: _syncFab() now checks whether #modal specifically is open rather than any element with class modal. The FAB now only appears on logging forms and other content rendered inside #modal.grid-column:1/-1 to span the full container width. The pattern already existed on Decompensation Warning, Pulse Pressure Detector, Symptom Convergence, Sentinel, and Pinned Events..hr-stat-card CSS rule contained text-center; — a Tailwind class name accidentally written as a CSS property. It is not valid CSS and was silently ignored, leaving stat card text left-aligned. Fixed to text-align:center;.<s> tags, displaying as struck-through text. On a medical checklist this reads as deleted rather than accomplished. Fix: completed items now display in bold with a ✅ checkmark and green background only — no strikethrough.#helpTabBar was registered without {passive:true}. Non-passive scroll listeners block the browser’s scroll thread and are flagged by Lighthouse. Fix: listener now registered with {passive:true}. The touchstart listener was already correctly marked passive._wzSaveChecklist() which was never defined. Paths B, C, and D correctly called _wzSaveChecklistPath(pathId). Fix: Path A now calls _wzSaveChecklistPath(‘A’) matching the pattern used by all other paths. The confirmation alert (“✓ Checklist saved!”) now appears correctly after tapping Save My Checklist on Path A..audio-alert CSS rule was missing its closing } before @keyframes pulse. The keyframe block was nested inside the unclosed rule, which is invalid CSS. Browsers silently discarded the animation declaration, leaving the alert box static when it should pulse. Fix: closing brace added after animation:pulse 1s ease-in-out infinite; so @keyframes pulse is correctly scoped at the top level.=== FLUID VS NEXT MORNING BP === section added to context. For each day with fluid logged in the window, the first BP reading of date+1 is looked up directly from allBP (already in scope) and output as a verified pair. A system prompt directive instructs the AI to use this table and never construct next-morning pairings from raw data.Date.getDay() was returning the wrong day-of-week value on this device. Fix: replaced Date.getDay() with Tomohiko Sakamoto’s pure-math day-of-week algorithm — computes the weekday directly from the year/month/date components with no reliance on the browser’s Date object timezone behavior.beforeAfterRe pattern inside _parseAskComparison. Queries like “since March 6, how many HR readings were above 80 bpm” were routed to the comparison context builder (which builds a before/after window) instead of the date range parser (which builds the correct open-ended since-March-6 window). The comparison context does not include the HR frequency table, so the AI could not answer threshold queries accurately. Fix: “since” removed from beforeAfterRe so it falls through to Pattern G in _parseAskDateRange as intended.buildAskContext() called getAllHistoricalData() exclusively to load readings. That function applies its own 90-day cutoff check that can differ from the query window set by inWindow() — causing it to return 0 in-period readings for dates that genuinely exist in localStorage (e.g. “7 days ago” correctly resolves to May 5 but getAllHistoricalData() misses those readings). Fix: after getAllHistoricalData() runs, the BP HISTORY section now performs a supplemental direct localStorage scan for every date that passes inWindow() but was not already loaded. This guarantees no readings are skipped regardless of any gap in getAllHistoricalData()’s cutoff logic._parseAskDateRange() identified 7 query types that returned wrong data or fell back to the 14-day default. All 7 are now fixed: N days ago (e.g. “3 days ago”) returns that specific day; last [weekday] (e.g. “last Tuesday”) returns the most recent occurrence; N weeks ago returns the 7-day window from that period; week of [date] returns the full Sunday–Saturday week containing that date; last year returns Jan 1–Dec 31 of the prior year; Q1/Q2/Q3/Q4 and first/second/third/fourth quarter return the correct 3-month window.new RegExp() strings in the comparison parser used single-backslash \s, \d, \w which JavaScript treats as literal characters, not whitespace/digit/word classes. Every comparison query — "February vs March," "before and after March 5," "what changed after March 6" — silently failed and fell through to the date range parser with wrong data. All five regex corrected with proper \\s, \\d, \\w escaping._askSessionDateFrom / _askSessionDateTo) and reused for follow-up questions that don’t specify a new date. Session resets when the panel closes or history is cleared._buildAskComparisonContext() only computed BP, symptoms, and fluid averages. Weight trend, medications logged, activity count, meal count, and note count are now included in both period A and period B stats.stop_reason field is now checked; when it equals max_tokens, a note is appended: “Response was cut off — ask ‘continue’ to see the rest.”AbortController cancels the request after 25 seconds with a clear message explaining the cause and suggesting a narrower date range.from: ‘2000-01-01’, making the AI believe it had 26 years of data. Fix: the pattern now scans localStorage for the actual earliest BP_TRACKER_YYYY-MM-DD key and uses that as the start date.useTo when useFrom was null, falling back to 14 days. Now handles all four combinations correctly: both bounds set, open start (since [date]), open end (before [date]), and no bounds (14-day default).useFrom was null. Now shows accurate label so the AI knows exactly what data it has and can flag period mismatches._parseAskDateRange() matched the month name “April” and consumed the year “2026” via the optional year group, returning April 1–30 instead of April 1. Fix: a new Pattern 4.5 inserted before Pattern 5 matches “Month DD [YYYY]” as a specific single-day window. Only fires when a day number is present alongside the month name; whole-month tokens (month name alone or with year only) still fall through to Pattern 5.buildAskContext() used windowMeals.slice(-20) to limit output. When the window contained many meals (e.g. a full month), slice(-20) returned only the most recent 20 — silently dropping earlier dates. For an April 1 query expanded to all of April, every April 1 meal was cut. Fix: meals are now sorted chronologically and all meals in the window are included (no slice cap), matching the behavior of the BP, symptom, and activity history sections.=== HISTORICAL FLUID ENTRIES === section reads the fluid, bp (f field), and meals (mealFluidOz) arrays from each historical day’s localStorage entry within the query window and lists each entry with date, time, amount, source type, and notes.dailyFluid was unconditionally pushed into the fluid statistics array before the window filter, inflating averages and day counts for date-specific queries (e.g. querying April 1 would show “2 days” of fluid data including May 11). Fix: today is only added to fluid stats when today’s date passes inWindow().touch-action:manipulation to all button CSS classes (body, .btn, .modal-btn, .modal-cancel, .modal-ok, .stat-tile). Eliminates the 300ms iOS WebKit touch delay and intermittent cancelled taps on all interactive elements app-wide.panel-notes, panel-rate) matching all other cards. Both added to CARD_PANEL_MAP with count-based tiles. Med Effectiveness tile always visible — adapts to setup state with educational nudge when medications not yet configured or no tagged readings exist..notes field. All Notes view extended to scan BP, Fluid, and Medicine log entries for notes alongside existing Weight, Symptom, and Activity coverage.closeAnalyticsDashboard() now calls collapseAllAnalyticsPanels() before hiding — next open always starts with a clean collapsed state.scrollIntoView with direct modalContent.scrollTop assignment at 150ms delay. Eliminates intermittent iOS failure where smooth scroll targeted the viewport instead of the modal scroll container.bottom:90px to bottom:24px. _syncFab() rewritten to manage Close FAB + Ask FAB + activity pill without any Today’s Status logic.manageMedicines() modal was fully functional and accessible via the Wizard and a hidden button on the main page, but there was no direct path in Settings. Fix: a dedicated 💊 Medications accordion section added to Settings between Baseline Conditions Registry and Heat & Humidity Advisory, with a single button that opens manageMedicines() directly._confirmImport() wrote all keys to localStorage then called refreshAllDisplays() — but never called loadAppointments(). The appointments[] in-memory array stayed at its pre-restore state, so updateRecentAppointmentWidget() always saw an empty array and showed “No appointments saved yet” even when cardiacAppointments was fully populated in storage. Fix: loadAppointments() added to the post-import refresh block, before refreshAllDisplays().visibilitychange handler has two branches: same-day resume (correctly called loadAppointments()) and date-rollover (did not). On iOS, the PWA JS context is killed overnight — appointments[] resets to [] at declaration. On morning resume, the date has changed so the rollover branch runs, skipping loadAppointments(), and the card renders empty. Fix: loadAppointments() added to the date-rollover branch.setInterval midnight auto-refresh cleared daily arrays and called refreshAllDisplays() but never called loadAppointments(). Fix: loadAppointments() added before refreshAllDisplays() in the midnight handler.appointments = [] but never called updateRecentAppointmentWidget(). The card continued to display the deleted appointment until page reload. Fix: updateRecentAppointmentWidget() called immediately after appointments = [] in the delete-all block.settings.features.meals / d.setup.features.meals. This flag is only ever set by the Getting Started wizard — users who track meals without completing wizard setup never have it. The Meals card itself has no dependency on this flag (it shows by default). Result: the PPH section never rendered and the nudge never fired for any user who did not run the wizard. Fix: both guards replaced with a check on actual meal log count (d.logs.meal.count ≥ 3 for the nudge; data existence check for the Sentinel section). If the user has logged 3+ meals the checks pass — no feature flag dependency.pph_pattern_detected companion card nudge moved from position 21 to position 2 in NUDGE_RULES so it is no longer blocked by behavioral nudges firing first. Also fixed: btnFn was 'openAdvancedAnalytics ? openAdvancedAnalytics() : null' — the function is named openAnalyticsDashboard(); corrected.snapshotSetup() stores d.setup.activeFeatures as an array of enabled feature keys, but never stored d.setup.features as the object the nudge rule was checking. The guard !d.setup.features.meals evaluated undefined.meals and always returned false, silently blocking the nudge regardless of how many PPH events existed. Fix: snapshotSetup() now also writes d.setup.features = features. The nudge guard updated to check d.setup.features.meals first, then fall back to d.setup.activeFeatures.indexOf('meals') !== -1 for existing installs that haven't re-snapshotted yet. The nudge will now fire correctly on the next Sentinel evaluation for users with 3+ PPH events and the average drop guard met.refreshAllDisplays() never called updateRecentAppointmentWidget() — on tab-resume and daily ticks the card went stale without the widget knowing. (2) The same-day path in the visibilitychange iOS resume handler called loadDailyData() but not loadAppointments() — on iOS PWA resume where the JS context was stale, appointments could be [] even though cardiacAppointments was present in localStorage. (3) deleteAppointment() called filterAppointments() to refresh the modal but never called updateRecentAppointmentWidget() — main-page card stayed stale after any delete. (4) updateRecentAppointmentWidget() set display:none when appointments.length === 0, making the card invisible on a fresh session before any data had loaded, which masked the other three bugs. Fix: card now always renders — empty state shows an “➕ Add Appointment” prompt; all three missing call sites added.getAllHistoricalData() was replaced with var passesFilter = true; // TEMPORARILY DISABLED FOR TESTING — bypassing the 90-day data window entirely and loading all historical data on every call regardless of age. This was never reverted before shipping. Additionally, 37 console.log('DEBUG...') and console.log('=====...') statements were left in the same function, flooding the browser console on every analytics call. Both removed: passesFilter restored to (keyDate >= cutoffDate && keyDate <= today); all debug log lines stripped.{bp, weight, symptoms, activities, meals, medicines, notes, procedures} but not breathing. Since breathing sessions are stored as a flat array under a single key (not per-day like BP data), they require a separate read. Fix: cardiaclens_breathing_sessions is now read once, filtered to the 90-day window by session date, and returned as breathing: allBreathing. This makes the data available to any future feature that reads the historical bus.buildAskContext() had no breathing section. Questions like “how have my breathing sessions been going?” or “what’s my average HR response?” returned no data. Fix: a new === BREATHING SESSIONS === section added at the end of context, including total session count, average HR response, sessions-with-decrease count, and a per-session line (date, technique, duration, before/after HR, delta, notes) for the 10 most recent sessions.breathing_try_it: fires when app age ≥14 days, 10+ BP readings, 5+ HR readings exist, but no breathing sessions have ever been logged. suppressDays: 30. Action button opens the Breathing Logger directly. breathing_declining_response: fires when 6+ sessions exist and the last 3 sessions average less than 50% of the first 3 sessions’ average HR response (baseline ≥4 bpm required to avoid false fires on weak responders). Prompts clinical discussion about pacemaker rate floor engagement. suppressDays: 14.showMealsHistory() and closeMealsHistory() both used mh.innerHTML = … to rebuild the entire header from scratch, and neither included the 📋 View History button in the rebuilt HTML. Fix: both functions converted to use openCardHistory() / closeCardHistory(), which update only the <span> text and leave all buttons intact. The static header already had a <span> as first child; no HTML changes needed.showWeightHistory() set weightViewMode = 'all' directly without calling claimHistorySlot('weight'), allowing Weight history to open while another card was already showing history. Every other card (BP, Symptoms, Activity, Meals, Medicines) calls claimHistorySlot first and blocks if another card is active. Fix: if(!claimHistorySlot('weight')) return; added as the first line of showWeightHistory().← Back to Today button in Weight history was missing -webkit-tap-highlight-color:transparent and min-height:44px present on every other card’s Back to Today button. Fix: both properties added to match the standard.renderPinSuggestion() always rendered the full suggestion box, then changed individual button states to “✅ Pinned” for already-handled events, but never hid the container when all suggestions were handled. Fix: added an allHandled pre-check at the top of renderPinSuggestion(). If every suggestion in the list returns true from isPinnedEvent() (checking both active and archive), the container is hidden immediately and no box is rendered. Applies to all four pin sections uniformly — no per-detector changes needed.pphEvents ≥ 3, regardless of what the population-level trend looked like. With 4 events out of 160 meal pairs and an average systolic change of −1 mmHg across all pairs, the banner was incorrectly firing on noise. Root cause: the single-threshold guard did not account for the fact that a small number of individually qualifying drops can clear the count threshold while the broader meal→BP relationship is flat or positive. Fix: a second guard added to both generatePPHAnalysis() and the Sentinel pph_pattern_detected nudge — the banner and nudge now require pphEvents ≥ 3 &AND; avgSysDrop ≥ 5 mmHg across all analyzed pairs. The 5 mmHg population-average threshold ensures the overall post-meal BP trend is meaningfully downward before a pattern is declared. The Sentinel nudge received the same calculation inline with its own pair-count and running-sum accumulators, matching the logic exactly.showSymptomsHistory() and closeSymptomsHistory() both used sh.innerHTML = … to rebuild the entire card header from scratch — and neither included the View History button in the rebuilt HTML. All other cards (BP, Weight, Activities, Meals, Medications) use the generic openCardHistory() / closeCardHistory() helpers, which update only the <span> text node and leave every button intact. Fix: both symptom history functions rewritten to use the same generic helpers. The static header HTML was already correctly structured with a <span> as first child; no HTML changes needed.archiveAndClearPins() wrote an empty array to cardiaclens_pinned_events and refreshed the modal and badge, but did not clear the four session-scoped detector cache variables (_decompGhostFire, _decompLastPoints, _decompLastActive, _decompCacheTs) or re-render the main-page rate detector. Fix: archiveAndClearPins() now nulls all four cache variables and calls renderRateDetector() before updateAnalyticsBadges(), ensuring the entire UI reflects the empty active-pins state immediately without requiring a page reload.<div> opening tag in the Planned Features section was split mid-attribute (border-ra truncated) with a second complete <div> tag appended on the same line — the same class of corruption test 15n guards against, but in a different form not matched by the existing regex. The browser parsed the two merged tags as one element, leaving an unclosed div inside Planned Features. (2) The "Shipped in v9.10.49 — Clinical Event & Appointment Intelligence" entry in Known Issues was missing its opening <div> wrapper entirely — causing its closing </div> to propagate upward and close a parent container element. This broke the DOM structure of every section that followed: content fell outside the .helpSection elements where JavaScript's setProperty calls could not reach it. No CSS or JS fix could resolve a structural DOM corruption. Fix: inserted the missing <div> wrapper around the orphaned entry; restored the truncated div opening tag to a single well-formed tag. Both sections now have balanced div structure verified by automated audit.hidden attribute removal, hs-visible class — were operating below the decisive level of the CSS cascade. The definitive fix: every section show/hide call now uses el.style.setProperty('display', 'block'/'none', 'important'). An inline style set via setProperty with 'important' priority sits at the absolute top of the CSS cascade — it outranks every author stylesheet rule, every UA stylesheet rule, every inherited style, and every !important in a class rule. No browser on any platform can override it. Applied consistently in all three code paths that control section visibility: setHelpSection, _helpShowAllSections, and helpGoToResult. The third path (helpGoToResult) was missed in all prior fixes and was silently re-introducing a normal inline style.display = 'block' whenever a search result was tapped — leaving a stale inline style that subsequent CSS rules could then fight over.<section> tags carried leftover hidden HTML attributes and style="display:none" inline attributes from all previous fix attempts. On iOS Safari the UA stylesheet and inline style together create a cascade layer that can outrank author !important rules in edge cases. These attributes were a second source of truth competing silently with the CSS. Second: the welcome section had no initial visibility class, so it relied entirely on JavaScript running after page paint to become visible — on slower devices or when the 500ms first-run timer fires, the modal could open to a blank white body. Both gaps fixed: all hidden attributes and style="display:none" inline styles removed from every .helpSection element; the welcome section now carries class="helpSection hs-visible" directly in the HTML so it is visible on first paint before any JavaScript runs. The CSS owns visibility exclusively with no HTML-level overrides competing against it._eventsWidgetCollapsed flag) — the full widget only returns when the user explicitly expands it.getAllHistoricalData() regardless of whether the user has meal tracking enabled. In environments where meal + BP data happened to produce 3+ PPH events, it would fire and suppress other nudge rules (including behavioral milestone cards). Fixed: guard added — d.setup.features.meals must be true before the historical data check runs. Clinically appropriate: PPH nudges should not surface for users who are not tracking meals.pph_pattern_detected fires when 3+ PPH events detected in 30 days. suppressDays: 14.el.style.display to show/hide sections. On iOS Safari, inline styles set by JavaScript participate in the CSS cascade in ways that interact unpredictably with class-based rules — display:none set by JS can be overridden silently by a CSS rule added later, or fail to override an existing inline style set by a prior JS call. The fix eliminates inline style manipulation entirely. Two CSS rules now own visibility exclusively: .helpSection { display:none !important } hides all sections by default; .helpSection.hs-visible { display:block !important } shows the active one. Because .helpSection.hs-visible has higher specificity (0,2,0) than .helpSection (0,1,0), it wins even with both rules carrying !important. setHelpSection and _helpShowAllSections now only call el.classList.toggle('hs-visible', isMatch) — no inline styles, no hidden attribute, no cascade conflict possible on any platform or browser..helpSection { display:block } was competing with the JavaScript section-switching logic — specifically overriding el.style.display = 'none' in browsers where CSS specificity wins over inline style on class-matching elements. The v9.10.64 three-layer fix (adding a .section-hidden { display:none !important } class) added unnecessary complexity. Simplified in v9.10.85: the .helpSection { display:block } rule has been removed entirely. setHelpSection reverted to the clean original approach — el.style.display = isMatch ? 'block' : 'none' plus setAttribute('hidden','') / removeAttribute('hidden'). No competing CSS rule means no override battle on any platform..helpSection { display:block } was overriding the JavaScript el.style.display = 'none' hide instruction on iOS Safari, leaving the Welcome section permanently visible regardless of which tab was tapped. Fixed with a three-layer approach: (1) new CSS class .section-hidden { display:none !important } which outranks the .helpSection class rule, (2) inline style.display = 'none' retained as backup, (3) setAttribute('hidden','') retained as backup. All three point the same direction — no browser on any platform can override them simultaneously.requestAnimationFrame and body.style.scrollBehavior manipulation to reset scroll position on tab switch. On iOS with -webkit-overflow-scrolling:touch, multiple programmatic scrollTop assignments via rAF permanently broke native momentum scroll — touch gestures passed through to the page behind the modal instead of scrolling the modal. Reverted to single synchronous scrollTop = 0. Changed helpModalBody from overflow-y:auto to overflow-y:scroll and added overscroll-behavior:contain to prevent touch bleed-through.setHelpSection iterates a hardcoded tabKeys array to set/clear active tab button states. breathing-logger was added to the tab bar in v9.10.59 but not added to this array — the tab button never cleared when switching away from it. Fixed in both tabKeys arrays. Regression guard test added to Section 15 of the test suite._addAskBubble was calling thread.scrollTop = thread.scrollHeight but askThread is not the scrollable container — helpModalBody is. The scroll did nothing. Fixed: now scrolls helpModalBody to the top of the assistant response bubble so the beginning of the reply is always visible.checkForUpdates() wrote its result to settingsVersionStatus, there was no scroll to bring it into view. Added scrollIntoView({behavior:'smooth',block:'nearest'}) for both the new-version and up-to-date result states.\\n sequences instead of real newlines — rendered as visible text in the modal body. Corrected.overflow:hidden to display:flex;flex-direction:column;max-height:90vh with the scroll area as flex:1 1 auto. (2) Test suite Section 61 added: 25 new tests covering the safety gate, all five prior bug fixes, context advisory DOM elements, override tracking, COPD check, low-response flag math, duplicate declaration checks, and disclaimer placement verification._breathRunContextGate() now calls _silentDecompAnalysis() before reading _decompLastPoints — previously the score could be 20+ minutes stale if the user hadn't triggered a badge refresh, meaning active warning signals could be missed entirely. (2) Duplicate var delta declaration in saveBreathingSession() — second declaration was dead code left from an earlier edit; removed. (3) Unused var now = new Date() in saveBreathingSession() — dead code; removed. (4) Low-response ⚠ flag fired incorrectly on negative deltas — sessions where HR increased were being flagged with the "⚠ <5 bpm" low-response marker alongside the orange ↑ indicator, which was contradictory. Flag now only fires when hrDelta >= 0 && hrDelta < 5 (weak positive response). (5) openBreathingHistory() did not hide breathingBlockView — if the block screen was visible and history was somehow opened, both views could render simultaneously. Fixed. Also removed startTime and pausedAt from breathState (declared but never written or read) and corrected a duplicate display:none in the custom duration row HTML._activityMinimized, _minimizedActivityState, and activityElapsedSeconds are in-memory variables only. When iOS suspends and resumes the JavaScript context, these reset to their initial values. The visibilitychange handler then calls loadDailyData() from localStorage — but the activity state was never written there, so it was unrecoverable. Fix: minimizeActivityLog() now writes the full activity state plus a minimizedAt timestamp to localStorage key CB_ACTIVITY_MINIMIZED. On visibilitychange resume, if _activityMinimized is false but the key exists, the state is restored and elapsed time is recalculated as stored seconds plus seconds elapsed while the app was away. A toast confirms the restore with activity type and elapsed minutes. CB_ACTIVITY_MINIMIZED is cleared on both saveActivity() and cancelActivityLog() so no ghost state persists after the session ends._decompLastPoints) with no freshness check — if the signal fired at load time then cleared, the badge stayed stale indefinitely until the modal was opened. (2) The pin card was rendered purely from live analysis state — no record of the firing event was preserved, so once the signal cleared the pin opportunity was silently lost. Fix: When analyzeDecompensation() scores ≥2 signal points, a session-scoped ghost record (window._decompGhostFire) is written with the active signal labels and today's date key. If the user later opens the modal and the live signal has cleared, _injectDecompPins() detects the ghost record and renders a muted “Earlier Today: signals fired” card with a full pin button — the window stays open for the rest of the day. Ghost expires at midnight via date-key comparison. Separately, updateAnalyticsBadges() now stamps a cache age timestamp (_decompCacheTs); if the cache is >20 minutes old it re-runs _silentDecompAnalysis() before setting the badge, keeping the badge accurate without requiring the modal to be opened.cbTrack() system: ask_panel_opened (each time the Ask panel is opened — measures session frequency), ask_question_submitted with session_q_count parameter (each question sent — counts total volume and questions-per-session depth), and ask_answer_received with session_q_count (each successful answer — measures completion rate vs abandoned questions). A per-session counter _askSessionQuestionCount resets each time the panel opens, enabling average questions per session to be tracked in GA4. This data supports future rate-limit decisions without guessing at real usage patterns.undefined/undefined in the data context sent to Ask, making all BP-related questions unanswerable. Root cause: buildAskContext() was reading r.sys, r.dia, r.hr, and r.time but the BP reading object stores these as r.s, r.d, r.h, and r.t respectively. The same mismatch affected the historical average BP calculation. Also fixed: weight time field (lw.time → lw.t). All BP data, heart rate, and weight timestamps now serialize correctly into Ask context.M array, field m for name, t for time) and up to 20 meals from the last 14 days (from getAllHistoricalData().meals) are now included in context.buildAskContext() revealed that every data type except fluid total and notes had wrong field names. Symptoms used s.name (should be s.symptom) — both today’s and 14-day history. Medications used m.name (should be m.medicine) and a nonexistent m.dose field (replaced with m.time). Pinned Events used p.date, p.doctor, and p.status which do not exist as top-level fields — corrected to p.eventDate, p.commLog.to, and p.commLog.status respectively. Pinned Event p.summary (the detector’s clinical summary text) and question responses (q.response) are now also included in context. All Ask questions involving symptoms (“what symptoms have I had?”), medications (“what did I take today?”), and pinned events (“what do I need to discuss with my doctor?”) now receive accurate data..modal elements for open-state, not the Ask panel. On iPhone PWA, scrolling within the fixed Ask sheet still fires window.scroll, which re-showed the FAB when scrollY > 300px because the Ask panel is not a .modal. Fix: scroll listener now also checks askPanel.style.display, matching the existing _syncFab() logic. Belt-and-suspenders: FAB z-index reduced from 999999 to 999985 (below the Ask backdrop at 999989) so it can never float above the Ask panel regardless of timing.Clinical Event & Appointment Intelligence — 4 New Nudge Rules — Closes the gap between clinical events that happen in the real world and the data CardiacLens needs to stay accurate. New helper function _openSentinelToEventLog() deep-links directly into Sentinel and opens the Clinical Event Log form, scrolling it into view (400ms deferred after render). Four new NUDGE_RULES: (1) pre_appointment_snapshot — fires when a saved appointment is 0–3 days away, no Snapshot saved in 7 days, and 14+ BP readings exist. suppressDays: 3. Action button opens Symptom Convergence → Timeline tab. (2) post_visit_clinical_event — fires when a saved appointment date was 1–4 days ago and no sentinel clinical event has been logged on or after that date. suppressDays: 5. Action button opens Sentinel Clinical Event Log form. (3) hr_shift_no_clinical_event — fires when a pinned HR level shift event (source: hr_detector) exists but no clinical event has been logged within 30 days of the pin date. suppressDays: 14. Action button opens Sentinel Clinical Event Log form. (4) clinical_event_log_unknown — fires when app age ≥30 days, 20+ BP readings, hasConditions, and cardiaclens_sentinel_events is empty — the user has never logged any clinical event. suppressDays: 30. Educational nudge, action button opens form. All 4 rules read cardiacAppointments and cardiaclens_sentinel_events directly from localStorage via try/catch, matching the existing pattern used by other rules. Gap 4 (reset-baseline reminder after deliberate non-reset) intentionally held — fires too close to a user choice and risks feeling intrusive. Test suite additions required (v12 → Section 52).
Getting Started — “Open Settings Now” button opened Settings hidden behind Help & About — The button called openSettings() directly. openSettings() writes to #modal and makes it visible — but #aboutModal (Help & About) was still displayed on top of it, hiding Settings completely. Fix: button now calls closeHelpModal(); setTimeout(function(){ openSettings(); }, 100); — the identical pattern used by every Settings-opening button inside the wizard (_wzGoSettings()). All other tappable elements in the Getting Started section were audited: wizard navigation stays within #aboutModal and is unaffected. No other buttons had this issue.
Getting Started Tab Rewrite — Progressive Onboarding — The Getting Started tab has been rewritten from a 1,200-word feature manual into a four-section progressive onboarding experience. Section 1: a warm three-step handshake (name, conditions, BP targets) with a direct “Open Settings Now” button. Section 2: plain-language guidance on what to log first — doctor-prescription framing, no feature inventory. Section 3: a short explanation of how Sentinel surfaces one suggestion at a time as data builds, so users understand they don’t need to configure everything upfront. Section 4: the full Setup Wizard repositioned at the bottom under “Already familiar with CardiacLens?” — preserved completely, just no longer the first thing a new user sees. Zero new JavaScript. Zero new logic. Zero conditional paths. The tab is static HTML — testable by eye on any device. Rationale: the behavioral nudge system is now the progressive onboarding engine; the Getting Started tab’s job is to earn trust and get the three critical data points in, then step back.
Behavioral Profile Layer — Accumulated Data Integration — Goal 3 of the v9.10.x behavioral intelligence sprint. Five changes shipped: (1) Streak rules corrected — streak_7_days and streak_30_days now use currentStreak directly instead of the old count/age approximation, which could fire for non-streak loggers. (2) fluid_never_logged nudge — fires when age ≥14 days, 20+ BP readings, hasConditions, and fluid.count = 0. Fluid tracking is a direct decompensation signal; this gap is clinically meaningful for cardiac patients. suppressDays: 21. Action button opens fluid log. (3) pulse_pressure_never_viewed nudge — fires when age ≥14 days, 20+ BP readings, hasConditions, and the Pulse Pressure detector has never been opened. suppressDays: 30. (4) appointment_never_saved nudge — fires when age ≥30 days, hasConditions, and engagement.appointmentsSaved = 0. First rule to use the engagement.* block. Without a saved appointment the Appointment Snapshot workflow is inert. suppressDays: 21. Action button opens appointment modal. (5) Instant-dismiss backoff in evalCard() — if cardsShown ≥ 8 and cardsInstantDismissed / cardsShown ≥ 0.75, nudge evaluation is skipped for that Sentinel session. Unlocks and encouragements still fire. First use of signals.* data in rule logic. Ratio self-corrects naturally as cardsReadAndDismissed grows. Test suite additions required (v12): fluid_never_logged fires/suppresses correctly; pulse_pressure_never_viewed fires/suppresses; appointment_never_saved fires/suppresses; backoff activates at threshold and lifts as ratio improves; streak_7_days uses currentStreak not count; streak_30_days uses currentStreak ≥30.
Baseline Conditions Cross-Reference — Each BP reading card now shows a personal baseline context pill when the reading is within configured thresholds (no red alert) but meaningfully elevated above the user’s personal quiet baseline. The pill appears in amber with the specific deltas: e.g. “↑ Sys +14 · HR +16 vs your baseline”. Thresholds: systolic ≥10 mmHg above baseline, diastolic ≥8 mmHg, HR ≥12 bpm — any one triggers the pill. Requires 14+ qualifying baseline days; never fires alongside a threshold alert. _sentMetrics() extended to return avgSys and avgDia; _sentComputeBaseline() extended to include both in the baseline object. New _getPersonalBaseline() session-cached wrapper computes once per 5 minutes and reuses across all readings in a render — avoids re-scanning 150 days per card. No changes to alert logic or Sentinel engine.
Heat & Humidity Sentinel Integration — The Heat Advisory system is now wired into CB_BEHAVIOR. Three additions to the behavioral schema: heat_advisory screen tracker (records manual advisory checks), heatAdvisoryAutoFired signal counter (increments each time an advisory fires automatically after activity logging), and heatAdvisoryEnabled in the setup snapshot (captured on every Settings save). Two new nudge rules: heat_advisory_conditions_disabled fires when the user has heat-sensitive conditions and 5+ activity logs but Heat Advisory is off — direct action button opens Heat Settings (suppressDays: 14). heat_advisory_feature_unused fires when the advisory is enabled and conditions are present but neither auto nor manual advisories have ever fired — guides the user to start using temperature bands during activity logging (suppressDays: 30). Both rules pass the cry-wolf test: they fire because of a safety behavior gap, not to add trigger volume.
Behavioral Intelligence — Setup Gap Rules + Unified Guidance — Three new companion card nudge rules now fire based on the CB_BEHAVIOR setup snapshot: setup_no_conditions (fires at 3+ days if no conditions set, suppressed 7 days), setup_no_patient_name (fires at 5+ days if name missing, suppressed 14 days), and setup_bp_not_customized (fires when 20+ readings logged and 5+ days old with default BP targets, suppressed 14 days). Each card includes a direct action button that closes Sentinel, opens Settings, and scrolls to the relevant section. The settings_complete encouragement rule now reads d.setup directly instead of calling _checkSettingsGaps() live. The standalone Settings Gap Card in Sentinel (v9.10.35) has been retired — setup guidance is now unified under the companion card system. snapshotSetup() is now called on app init (500ms deferred) so the setup block is populated from day one, not only after the user visits Settings.
Behavioral Intelligence Layer — Schema v2 Redesign — The silent behavioral observer has been redesigned from the ground up. CB_BEHAVIOR now tracks five dimensions: presence (screens and log types used), frequency and recency (with streak tracking and time-of-day distribution per log type), quality (notes attached to logs, context saves), setup completeness (snapshotted on every Settings save — patient name, conditions, daily events, medications, BP target customization), and engagement depth (anchor investigations saved, Doctor’s Reports generated, analytics notes, context notes, appointments, data exports, wizard completion). Signal response tracking records whether companion cards are read before dismissal or instantly dismissed, giving the rule engine a measure of its own effectiveness. 9 new hooks added. Full forward migration from v1 installs with zero data loss. No visible UI changes.
Activity Timer — Background Mode — A timed activity can now be minimized to a persistent blue pill in the bottom-left corner of the screen (mirroring the Today’s Status button on the right). Tapping “↙ Minimize — keep timer running in background” inside the timer display collapses the activity modal, saves all form state (activity type, notes, exertion, temperature band), and continues the timer silently. The blue pill shows the activity icon, name, and live elapsed time. The user can then log BP, check Sentinel, or do anything else in CardiacLens without losing their ride time. Tapping the pill reopens the activity modal with the exact accumulated time, all fields pre-restored, and correct timer button states. If the user taps Log Activity while a background activity is running, a choice prompt appears: Resume & Finish the current activity, or Discard & Start New. The activity pill stays visible even when other modals are open so the user always knows the timer is running. Reminder queue badge moved to bottom:82px to clear both pills. Today’s Status FAB behaviour unchanged.
Sentinel — Fluid Tracking Gap Detection — Sentinel now monitors fluid logging patterns and surfaces a single card when a meaningful gap is detected. Two scenarios are covered: (1) Event deletion drop — when a daily event with a fluid goal is removed from Settings, Sentinel compares today’s fluid total against the 7-day average and, if the gap is significant, shows a card with “Log Fluid Now” and “Restore Event” buttons. (2) Silent fluid day — after 7 PM, if the user has logged fluid on 5 or more of the last 7 days but today’s total is zero, Sentinel surfaces the same card. Both checks are gated to once per 24 hours so the app never nags. The fluid events snapshot is saved every time Settings are saved and on app startup so deletion detection is always current.
Sentinel — Settings Gap Card — Sentinel now checks for high-impact incomplete settings and surfaces a single non-intrusive card at the top of the Sentinel panel. The card identifies up to three gaps in order of clinical importance: patient name missing (affects Doctor’s Report header), no conditions selected (affects symptom matching and detector context), no daily events configured (fluid and BP reminders inactive), and no medications listed (effectiveness tracking inactive). Each gap has one button that closes Sentinel, opens Settings, and scrolls directly to the relevant section. The card has a dismiss button that hides it for 14 days. It never appears more than once every 14 days, and disappears permanently once all gaps are resolved. Does not show until Sentinel has built a baseline (7+ days of data).
Notifications — reminder no longer interrupts mid-log work — When a scheduled event reminder fired while the user was filling in a log form (BP, weight, symptoms, activity), showEventReminder() called showModal() which directly overwrote modalContent.innerHTML, destroying any partially entered data. Fixed: showEventReminder() now detects when the main modal is already open and queues the reminder instead of interrupting. A non-intrusive green badge appears at the bottom of the screen showing the event name with “Show Now” and “Skip” options. The OS chime still plays once. When the user finishes their log and closes the modal, the queued reminder appears automatically. No data is ever lost.
Notifications — advance chime ignored notify=false on Plan My Day events — The advance pre-warning system (checkScheduledAlerts) pushed all dailyPlan events into its fire queue without checking whether the user had turned off notifications. Events with notify: false were chiming early anyway. Fixed: dailyPlan events with both notify: false and sound: false are now skipped.
Notifications — advance chime double-fired after page reload — firedEvents (the advance-reminder tracking object) was stored only in memory. If the app was reloaded or the PWA woke from background after an advance chime had already fired, firedEvents was empty again and the chime re-fired. Fixed: firedEvents is now persisted to localStorage under BP_TRACKER_FIRED_EVENTS and restored on init alongside firedReminders.
Notifications — OS banner could fire after in-app reminder was already dismissed — Plan My Day event IDs were constructed with array index (plan_0_Name) in three different places, with no guarantee the index at scheduling time matched the index at dismissal time. Fixed: all three systems now use a shared _planEventId(evt) helper that builds the ID from name + time instead of index, guaranteeing consistent tags across the full scheduling → fire → dismiss cycle.
Notifications — “Not Now” permanently blocked future permission prompts — promptPWANotificationPermission() wrote the CARDIACLENS_NOTIF_PROMPTED flag before showing the modal. Tapping “Not Now” left the flag set, permanently blocking the prompt. Fixed: flag now written only when the user taps “Allow Reminders.” A “Re-enable Reminder Prompt” button appears in Settings → OS Notifications for users who previously tapped Not Now.
Sentinel Doctor View — Symptom Frequency now shows actual symptoms — When a cardiologist asks “what were the symptoms?” the Symptom Frequency Doctor View now shows a full list of every symptom logged in the last 7 days, grouped by date, with symptom name, severity score (color-coded), time logged, and any notes. Cardiologist can see not just “3.3 per day” but exactly what was logged, when, and how severe.
Help & About — tab bar scroll chevrons — A tappable ’ › ’ chevron now appears on the right edge of the Help tab bar whenever more tabs are hidden off-screen. Tapping scrolls the bar right by 200px with smooth animation. A ’ ‹ ’ chevron appears on the left when scrolled away from the start. Both chevrons auto-hide when their respective edge is reached. Chevrons use a gradient fade so hidden tabs are visually implied. Large tap targets for senior users. Built for the 14-tab bar where 5–6 tabs are typically off-screen on mobile.
Doctor View — symptom duplicates — getAllHistoricalData() already includes today’s saved symptoms. The Doctor View code was also adding the in-memory S array on top, doubling every symptom logged today. Fixed by removing the S merge — getAllHistoricalData() is the single source of truth.
Sentinel Doctor View — wrong stream shown — doctorLines[i] was mapped to score.streams[i] for stream key lookup, but doctorLines only contains drifting/alarming streams while score.streams contains all streams. Index mismatch caused the first drifting stream to always open HR Daily Spread Doctor View regardless of which sentence was tapped. Fixed: replaced parallel arrays with paired doctorEntries array of {line, key} objects built directly from each stream as it is processed, guaranteeing each sentence button opens the correct stream’s Doctor View.
Doctor View — HR pins still showing ? after repair — _dvBuildHRContent() read only beforeMean/afterMean (level_shift fields), but rapid_change pins store values in from/to fields. Result: all Rapid HR Change pins showed ? even after auto-repair. Fixed to resolve both field names with fallback. Also corrects the section heading (shows “Rapid Heart Rate Change” vs “Heart Rate Level Shift” based on type), and shows “Single session / within one day” instead of blank shift date for rapid changes. Repair key bumped to v2 to force re-run on all existing devices.
Doctor View migration notice — Users upgrading from versions prior to v9.10.25 who have existing pins with empty detectorData now see a one-time blue notice at the top of Pinned Events. The notice explains that existing pins still work for appointment read-aloud, but re-pinning will unlock the full “Tap to Show Your Doctor” data view. Includes a count of affected pins and step-by-step re-pin instructions. Dismissed permanently via “Got it” button stored in localStorage. Notice auto-hides permanently once dismissed and never reappears. Also auto-hides if all pins already have detectorData.
Doctor View — auto-repair of existing pins — On first open of Pinned Events, silently reconstructs detectorData for any existing pins with empty detector data by parsing their summary strings. Covers all four detector types: HR level shift (extracts before/after bpm averages, shift magnitude, direction), HR rapid change (from/to bpm, delta, direction), decompensation (signal names and count), pulse pressure (avgPP, chronic flag, status title), and symptom convergence (symptom names, average severity). Runs once, never again. No user action required — Doctor View data will populate automatically on next open of Pinned Events.
Doctor View — overlay hidden behind Pinned Events modal — Doctor View overlay had z-index:99999, identical to the .modal CSS class. Since Pinned Events modal appears later in DOM order, it won the z-index tie and rendered on top of Doctor View. Fixed: Doctor View z-index raised to 999999, above all modals.
Doctor View — “Symptom detail not available” — handlePinTap() was passing {} as detectorData to pinEvent(), discarding the symptom objects, HR shift values, and signal lists that detectors attached to each suggestion. Fixed: renderPinSuggestion() now stores all suggestion objects in window._pinSuggestionMap keyed by source|eventDate. handlePinTap() retrieves the correct detectorData from that map before saving the pin. All future pins will have full detectorData for Doctor View display.
Doctor View — Read-only data overlay with exact scroll restore — When a cardiologist says “show me,” the patient now taps a full-width “📊 Tap to Show Your Doctor” button (min 56px height, senior-safe tap target) on any sentence in the Sentinel or Pinned Events “What to Say” box. A full-screen read-only overlay opens instantly showing the clinical data behind that specific sentence — HR regime shift with before/after values, decompensation signals, pulse pressure trend, or symptom cluster with severities and timestamps. A large fixed “← Back to CardiacLens” header button (64px, always visible) returns to the exact scroll position. No inputs, no logging, nothing to accidentally tap. Doctor View is completely locked to read-only while open.
closeAbout() instead of closeHelpModal(). The floating Today's Status button did not restore correctly after closing. Fixed.e, E, +, and - as valid browser scientific notation. One global listener now blocks them across all 30+ numeric fields.What is actively in development or on the near-term roadmap. Features are prioritized by clinical value and user feedback.
For bugs that were fixed, see the Known Issues tab. Release history is at the bottom of this tab.
Once enough check-in data exists, a "Your Fluid Response Profile" view in Settings shows your personal pattern in plain language. This becomes the foundation for a productive conversation with your cardiologist about a personalized daily limit.
Barometric pressure drops correlate with PPH episode severity. CardiacLens will detect significant pressure changes and issue a proactive heads-up so you can adjust hydration and activity before symptoms occur.
Optional read-only access for a spouse, adult child, or caregiver to view your dashboard and receive alerts — without being able to edit your data.
Correlate meal logs with post-meal BP readings to identify which foods or meal sizes consistently trigger PPH episodes. Over time, builds a personal map of your dietary BP triggers.
Log sleep quality and duration. Poor sleep is a major driver of next-day BP irregularities and OSA complications. Correlating sleep quality with BP trends gives your doctor a fuller clinical picture.
Full native apps with reliable background notifications, HealthKit / Google Fit integration, Apple Watch complications, and wrist-tap logging. No browser required.
Auto-import heart rate and activity data from Apple Watch, Fitbit, and Garmin — so you are not re-entering data that your devices already captured.
CardiacLens is built by a patient, for patients. If there is something your care plan requires that CardiacLens does not yet do, we want to know. Use the Contact & Feedback section in Settings to send a suggestion directly to the development team.
Events Tile Queue + Sound Timing. Today's Status is the command center: overall status first, then one Events tile that changes color if something is due or missed. Tapping Events opens the queue in time order with Log, Snooze 10, and Dismiss for due/missed items. The app does not show large event cards on the home page, does not open catch-up modals on launch, and does not make a sound just because the app opens.
Food Check. Single-food nutritional awareness tool inside Ask CardiacLens. Access via the 🍴 tools icon in the Ask input bar. Enter food name, preparation method (19 options in 4 clinical groups), portion, and optional label values for Sodium, Potassium, and Phosphorus — each with a mg / %DV toggle. Ask evaluates the food against your logged conditions and medications and returns ✅ Generally fine, ⚠️ Worth watching, or 🚫 High concern with a profile-specific summary. Last-entered values are remembered for quick re-evaluation with changed variables.
Meal Builder. Full-meal nutritional analysis inside Ask CardiacLens. Build complete meals item by item with the same form fields and mg/%DV toggles as Food Check. Running totals for Sodium, Potassium, and Phosphorus update live as items are added. Analyze Meal evaluates the combined load — not individual items. After analysis, a Modify & Re-analyze button returns to the builder with all items intact; Save to Library saves the meal — if you got here by tapping ✏️ Edit on an existing saved meal, this updates that meal in place (the button reads 💾 Update), otherwise it adds a new meal to your library. Individual item correction is done by removing and re-adding the item with corrected values.
Saved Meals Library & Manage Meals. Saved meals are stored in CardiacLens data and included in all backups. Each saved meal records its items, combined nutritional totals, saved date, and last evaluated date. Manage Meals button on the main page (alongside Manage Medicines and Manage Symptoms) opens the library. Each meal card has four actions: ✏️ Edit (opens in builder pre-loaded), 🔄 Re-evaluate (sends to Ask against current conditions/medications), 🚫 Do Not Use (flags with optional note and hides from Log Meal picker), 🗑️ Delete. Saved meals also appear as a picker at the top of the Log Meal modal, organized by type, for fast pre-fill logging.
Day Summary & Nutrition Targets (v9.10.187). The top of Manage Meals shows a live nutritional summary. If no library meals are logged it explains how. Once logged, it shows sodium, potassium, and phosphorus totals. If daily targets are set in Settings, each nutrient shows a traffic-light indicator: green (under 75%), amber (75–99%), red (at or over limit). Tap amber or red to see per-meal breakdown and lower-nutrient options.
Ask panel UI restructure. The Ask input bar now has a single 🍴 tools icon that opens a two-option picker (Food Check or Meal Builder). Each tool takes the full panel height with a Back arrow to return to the Ask thread. Eliminates the previous multi-button row that caused scrolling and usability issues on mobile.
Log Medicine — conditional note shown at logging time. Medicines with a clinical condition (e.g. Metoprolol “Only when diastolic >70”, Losartan “Only when systolic >100”) now show an amber warning banner in the Log Medicine modal. Previously the note was only visible in Manage Medicines.
Pinned Events — “What to Say” box showing on Addressed pins — Language engine was checking evt.communications instead of evt.commLog. Fixed to read evt.commLog.status directly. Box now correctly hidden when status is Addressed.
Archived events reappearing as new pin suggestions — isPinnedEvent() now checks both active pins and the archive. An event that has ever been pinned and archived will never surface as a new suggestion again.
Sentinel — “What to Tell Your Doctor” box — When any stream is drifting or alarming, Sentinel now displays a highlighted box at the top of the modal with pre-written plain-language sentences the user can read aloud at their appointment. Box is hidden on All Clear. Orange border for drifting, red for alarming.
Pinned Events — “What to Say at Your Appointment” language engine — Every detector-backed pin card now displays an auto-generated sentence the patient can read aloud to their cardiologist. Language derived from detector data. Box hidden when Addressed, muted when Deferred, fully visible when Unresolved or Follow-up Ordered. Manual pins never get a box.
Help & About tab bar — horizontal scroll layout — Tab bar now scrolls horizontally in a single row. Full content height is always preserved regardless of tab count. Standard mobile UX pattern — swipe the tab bar to reach any section. Scales to any future tab additions without layout impact.
Sentinel documented in Help & About — Dedicated tab covers what Sentinel watches, what a Footprint is, how the Clinical Event Log works, and why personalized pre-event pattern recognition is clinically novel. Modal footer and disclaimer now always visible. Grammar corrected.
Sentinel v2 — Two-tier scoring drives alarms from 14-day recent pattern. Clinical Event Log with baseline reset. Stream cards in plain language with specific action guidance and date ranges.
Sentinel — Personal Pattern Precursor Detector — Watches 3 live data streams (HR daily spread, symptom frequency, pulse pressure) against your personal baseline. Learns from your Footprints — 72-hour pre-event windows extracted from all Pinned Events. Badge turns yellow at 2 drifting streams, red at 3. Pin any match for your Doctor's Report. Built from your real data: 9 Footprints across 45 days.
Sentinel — Personal Pattern Precursor Detector — Watches 3 live data streams (HR daily spread, symptom frequency, pulse pressure) against your personal baseline. Learns from your Footprints — 72-hour pre-event windows extracted from all Pinned Events. Badge turns yellow at 2 drifting streams, red at 3. Pin any match for your Doctor's Report. Built from your real data: 9 Footprints across 45 days.
Pinned Events question UI — three interaction bugs fixed — Add Question now shows the saved question immediately. Mark Asked checkbox now reflects state instantly. Delete (✕) now confirms before removing and always takes effect. All three operations re-render the pin card in place.
Button spacing audit — all main logging modals — Systematic pass through Log BP, Log Fluid, Log Activity, Log Symptom, Log Weight, Log Meal, and Morning Check-In. Minimum 12 px gap enforced between every pair of tappable buttons. Reduces misfire taps for senior users on touchscreens. TestSuite v4 Section 15 (Help & About, 18 new automated tests) released alongside.
Help & About — complete tab overhaul — Sticky tab bar. Larger tap targets. Pinned Events and Heat Advisory tabs added. Pinned Events tab includes full clinical context, doctor workflow, and anonymized case study.
Pinned Events comm layer UI freeze fixed — All 6 operations now in-place with toast. No modal rebuild. Smooth on mobile.
Pinned Events Communication Layer — Log Communication (who, date, response, status) and Add Question per-pin. All data archives with the pin.
All Notes search box fix — Multi-character keywords now work. _filterNotesInPlace() handles live search without destroying the input element.
Help & About documentation catch-up and Already-linked BP card dismiss button.
Recent BP contextual card at Log Activity open. BP Before Activity button in Manual mode. Manual mode info card.
Symptom System Full Overhaul — 70+ synonym mappings, Bulk Rename, clinical context prompt, Manage Symptoms rebuilt in 3 sections.
Tier C Pattern Ripening Nudge and Tier D Urgent Convergence Alert. Glossary Tab. Help & About full rewrite with QR code.
Four detectors live: HR Rate Detector, Decompensation Warning, Pulse Pressure Detector, Symptom Convergence. Pinned Events, 60-day Timeline, Appointment Snapshot, Archive & Clear.
Per-card history isolation. Weight outlier flagging (AHA/ACC thresholds). Weight jump-to-date picker. Critical launch-day regression fix.
Activity Timer Pause/Resume. HR Rate Detector Quick Stats Period Selector. All Notes emoji header fix.
Advanced Analytics analyzes BP readings before and after logged meals. Flags events with ≥15 mmHg systolic drops. For informational tracking only.
Postprandial Hypotension (PPH) is a drop in blood pressure that occurs after eating. "Postprandial" means after a meal; "hypotension" means abnormally low blood pressure.
The clinical definition: A drop of ≥20 mmHg systolic within 2 hours of eating. This is the threshold used by cardiologists and published in clinical guidelines.
Why it matters: PPH is common in older adults and in people with autonomic nervous system conditions, diabetes, Parkinson's disease, and certain cardiac conditions. It can cause dizziness, lightheadedness, falls, and fainting. Many people experience it without realizing meals are the trigger.
When you eat, your body redirects significant blood flow to your digestive tract to absorb nutrients. In a healthy cardiovascular system, compensatory mechanisms — heart rate increase, blood vessel constriction elsewhere — maintain overall blood pressure. In people with autonomic dysfunction or compromised cardiac output, these compensatory responses are slower or weaker, and blood pressure drops before the body can correct it.
Contributing factors include: large meals (more blood required), high-carbohydrate meals (faster digestion, faster demand), alcohol with meals, being dehydrated before eating, and certain medications (especially antihypertensives taken near mealtime).
The Post-Meal BP Patterns panel in Advanced Analytics automatically analyzes your logged data using the clinical standard:
Each meal pair is shown in a table with the pre-meal reading, nadir BP, systolic drop, and how many minutes after eating the nadir occurred. PPH events are marked with ⚑.
Step 1 — Log your meal using the 🍽️ Log Meal button. The timestamp is recorded automatically when you save.
Step 2 — Log BP before eating (within 60 minutes before your meal, while still at rest). This is your baseline. Sitting quietly for 3–5 minutes before taking it gives the most accurate pre-meal reading.
Step 3 — Log BP after eating at roughly 30, 60, and/or 90 minutes post-meal. CardiacLens uses the lowest reading in the 2-hour window — so even one post-meal reading helps, but multiple readings improve accuracy by capturing the true nadir.
Step 4 — Open Advanced Analytics and scroll to Post-Meal BP Patterns. You need at least 3 meals and 5 BP readings in the selected time period before analysis runs.
If you experience any of these within 1–2 hours of eating, log them in CardiacLens immediately and note the meal timing:
Note: PPH can be asymptomatic — a significant drop may occur with no symptoms at all. The only way to know is to measure.
When 3 or more meal pairs are available, your Doctor's Report automatically includes a Post-Meal BP Pattern (PPH) section summarizing the number of PPH events, average post-meal BP change, and (when a pattern is confirmed) a list of the most recent PPH events with dates, meal names, and drop values. This gives your cardiologist documented evidence — not just a complaint of "feeling dizzy after meals."
For informational tracking only. CardiacLens does not diagnose PPH. Discuss any detected pattern with your cardiologist — they can determine clinical relevance, rule out other causes, and advise on management strategies.
Plain-language definitions for every clinical term and app phrase used in CardiacLens. If you see a word in the app and don't know what it means, it's here.
The force your heart uses to push blood through your arteries, measured in millimeters of mercury (mmHg). Expressed as two numbers: systolic (the top number — pressure when your heart beats) over diastolic (the bottom number — pressure when your heart rests between beats). Example: 120/80 mmHg. Normal range for most adults is below 130/80, but your cardiologist will give you a personal target that may differ.
The difference between your systolic and diastolic readings. Example: if your BP is 118/85, your pulse pressure is 33 mmHg. Normal range is 40–60 mmHg. A narrow pulse pressure (below 40) can indicate the heart is working harder to push blood forward — a pattern seen in reduced cardiac output and certain valve conditions. A wide pulse pressure (above 60) may indicate arterial stiffness. Many cardiac patients have a chronically narrow PP that is their personal baseline — what matters is change from that baseline, not comparison to textbook normals.
How many times your heart beats per minute (bpm). Normal resting range is 60–100 bpm. Below 60 is bradycardia (slow heart rate); above 100 is tachycardia (fast heart rate). Patients with pacemakers may have a lower set rate limit — your pacemaker clinic can tell you what range is expected for your device settings.
A heart rate below 60 bpm. In CardiacLens, the HR Rate Detector flags readings below 50 bpm specifically, as this is below the range most pacemakers are programmed to allow. Bradycardia is normal for well-trained athletes and some medication effects (beta-blockers, for example, intentionally slow the heart rate). Context matters — discuss any persistent low readings with your cardiologist or device clinic.
A sustained change in your heart rate baseline — where your resting HR was consistently at one level, then durably shifted to a new level and stayed there. This is different from a single high or low reading. CardiacLens's HR Rate Detector identifies these shifts statistically by comparing before-and-after windows for stability. A level shift is clinically significant because it suggests something changed in your cardiac state — compensation, medication effect, disease progression, or recovery. Your cardiologist will want to know.
A chronic condition where the heart doesn't pump blood as efficiently as it should. "Congestive" refers to fluid backing up into the lungs and body (edema). Heart failure doesn't mean the heart has stopped — it means it's working harder than it should to maintain output. Management focuses on monitoring fluid balance, weight, symptoms, and medication adherence. CardiacLens's Decompensation Warning system is specifically designed to detect early signs of fluid buildup before symptoms become severe.
When a cardiac patient's condition worsens — typically due to fluid overload in CHF — to the point where symptoms become severe and hospitalization may be required. Early decompensation signs include rapid weight gain (fluid retention), worsening shortness of breath, swelling in legs and ankles, fatigue, and reduced activity tolerance. The goal of CardiacLens's Decompensation Warning detector is to surface multiple early signals before they progress to a crisis.
In the Decompensation Warning detector, each of the five clinical signals (weight, cardiac output, symptoms, fluid, notes) can score 0, 1, or 2 points based on severity. Maximum total is 8 points. The detector shows how many points are active and which signals contributed. One point alone is labeled "Early Indicator" — not alarming. Two or more points prompt closer monitoring. Two or more red-level signals prompt contacting your cardiologist.
When multiple symptoms occur together or escalate simultaneously in a pattern that suggests an underlying common cause. In CardiacLens, convergence is detected across three layers: symptoms clustering within a 4-hour window on the same day, the same symptom worsening in severity over multiple days, and recognized clinical symptom triads. The significance is that individual symptoms are often dismissed — but a convergent pattern of multiple symptoms is harder to attribute to chance and more likely to represent a real clinical event.
A recognized combination of three symptoms that together suggest a specific condition or event. CardiacLens recognizes: the CHF Decompensation Triad (shortness of breath + fatigue + swelling/edema — a classic early decompensation pattern); the Arrhythmia Triad (palpitations + dizziness + chest pressure/tightness — may indicate an arrhythmia episode, note timing and duration); and the Systemic Symptom Pattern (nausea + fatigue + reduced appetite — nonspecific, worth noting but not alarming on its own). Triads are worth mentioning to your cardiologist, not reasons to panic.
A drop in blood pressure that occurs after eating. "Postprandial" means after a meal; "hypotension" means low blood pressure. Common in older adults and those with autonomic nervous system conditions. Typically defined as a drop of ≥20 mmHg systolic within 1–2 hours of eating. Symptoms include dizziness, lightheadedness, and fainting. CardiacLens's Post-Meal BP Patterns detector in Advanced Analytics uses the clinical standard — a ≥20 mmHg systolic drop within 2 hours of eating — to identify PPH events and surface patterns for discussion with your care team.
An irregular heart rhythm — the heart beats too fast, too slow, or with an irregular pattern. Common types include atrial fibrillation (AFib), where the upper chambers quiver instead of beating regularly, and various bradycardias and tachycardias. Symptoms can include palpitations (feeling your heart beat irregularly or rapidly), dizziness, shortness of breath, or fatigue. Some arrhythmias are asymptomatic. CardiacLens logs heart rate with each BP reading; rapid changes and low readings are flagged for review.
CardiacLens stores 90 days of daily readings on your device. After 90 days, the oldest day is removed as a new day is added — like a rolling 90-day window. This means data from more than 90 days ago is no longer in the app unless you exported a backup at that time. To preserve data long-term: export quarterly (last day of each quarter is a good practice). Pinned Events and Appointment Snapshots are exceptions — they are stored permanently and never roll off.
A clinically significant finding that you've chosen to permanently preserve. When a detector flags something worth showing your cardiologist, tapping "📌 Pin" saves the finding, the date, and all raw data from that day to a permanent list that never rolls off. Pinned events appear in your Doctor's Report and on the 60-day Timeline. After your cardiologist appointment, use "Archive & Clear" to save all current pins to a permanent archive and start fresh.
Your personal "normal" — the consistent pattern your readings follow when nothing unusual is happening. CardiacLens uses the word "baseline" in two contexts: (1) Your personal BP/HR baseline — the average range your readings cluster around, used as the reference point for detecting changes; and (2) Baseline symptoms in the Morning Check-In — symptoms you experience routinely (such as chronic ankle swelling after a procedure) that are part of your normal state, not new developments. Baseline symptoms are recorded for your doctor but excluded from pattern correlation calculations.
CardiacLens's core accuracy principle. The detectors use real statistical methods on real data — but statistics identify patterns, not causes. A detected HR level shift is mathematically real. Whether it represents disease progression, a medication effect, recovery, or random variation requires clinical judgment that only your cardiologist can provide. CardiacLens points in a direction and gives you the evidence to have a productive conversation — it does not tell you what that pattern means for your specific medical situation.
CardiacLens's design philosophy. Many health apps generate advice or recommendations — telling you what to do. CardiacLens instead verifies patterns across multiple data streams before surfacing a finding. It only escalates when signals converge — when two or more independent data streams agree that something changed. A single reading doesn't trigger an alert. A single signal point doesn't trigger a pin suggestion. This "verification first" approach is designed to prevent false alarms and preserve the credibility of findings that do get surfaced.
Every other cardiac monitoring tool watches for what is happening right now. Sentinel watches for what came before. It is built on a simple but powerful idea: the patterns in your own data in the days before a clinical event are more meaningful than any population-based threshold — because they are yours.
Sentinel watches three data streams around the clock — your HR daily spread, your symptom frequency, and your pulse pressure. It compares each one not to a textbook average, but to your own recent personal pattern. When two or more streams drift above your recent normal at the same time, Sentinel surfaces a pattern match — before symptoms escalate, not after.
Your recent pattern is calculated from your own logged data — the last 14 days, or from a clinical event date if you have logged one. This means Sentinel stays calibrated to where you are in your health journey, not where you were six months ago.
Each stream card on the Sentinel screen now includes a small line chart of your last 14 logged days, with a dashed horizontal line marking your recent normal. The "Your recent normal" vs. "Right now" numbers above it are two snapshots — the sparkline shows everything in between, so you can tell at a glance whether today's reading is part of a steady pattern or a single outlier on an otherwise volatile line.
Underneath each sparkline, a short caption compares the earlier half of the 14-day window to the more recent half and tells you, in plain language, whether the stream is holding steady, trending toward your normal, or drifting away from it — and whether that shift is large enough to matter compared to your own day-to-day variability ("noise"). You don't need to interpret the shape of the line yourself; the caption does that for you.
For Pulse Pressure, a wider reading is the favorable direction (narrow PP suggests reduced stroke volume — relevant for CHF and diastolic dysfunction). For HR Daily Spread and Symptom Frequency, narrower/fewer is favorable. The caption already accounts for this — "trending up" isn't automatically good or bad, it depends on which stream you're looking at, and the caption's wording reflects that.
When Sentinel detects that two or more of your streams are drifting above your recent normal, it automatically generates a "What to Tell Your Doctor" box at the top of the Sentinel screen. Each flagged stream gets one plain-language sentence you can read aloud at your appointment.
The language is read-only and auto-generated from your data. Sentinel stands behind every sentence it generates.
Every time you pin a clinical event from one of CardiacLens's detectors, Sentinel automatically extracts what your data looked like in the 3 days before that event and saves it as a Footprint. Over time, Sentinel builds a personal library of your pre-event signatures.
When your current data resembles a prior Footprint, Sentinel tells you specifically — which event it resembles, what your data looked like then, and what your data looks like now. It does not tell you what is going to happen. It tells you that a pattern you have lived through before is appearing again.
Pacemaker adjustments, new medications, procedures, hospitalizations — any clinical change that legitimately shifts your baseline belongs in the Clinical Event Log inside Sentinel. You will find it by opening Sentinel and tapping + Log Clinical Event.
When you mark an event as a baseline reset, Sentinel compares your streams to your data after that event — not before it. This is critical after a pacemaker adjustment, for example: your heart rate pattern may shift significantly as settings take effect, and Sentinel needs to know that the shift is intentional and clinical, not an emerging problem.
The Clinical Event Log is also a permanent record of your care timeline — visible in Sentinel, yours to keep, never transmitted anywhere.
No published cardiac monitoring tool has done this at the individual patient level outside a hospital setting. Academic research on pre-decompensation pattern recognition requires Holter monitors, IRB approvals, and study populations measured over months or years. The results are then averaged across those populations — which means the thresholds apply to the average patient, not to you.
Sentinel does it differently. It uses the data you are already logging every day. It compares you to yourself. Your quiet baseline, your pre-event Footprints, your recent pattern — everything Sentinel knows comes from your own history, not from a population study. That is what makes it personal in a way that published thresholds cannot be.
Sentinel is not a diagnosis. It is not a medical device. A pattern match does not mean something is wrong — it means your data looks similar to a prior period that preceded a clinical event in your own history. That is worth noting. It is not worth panicking about.
Sentinel provides the documented evidence and the pattern recognition. Your cardiologist provides the clinical interpretation. The goal is to walk into your next appointment with something real to show — not a vague feeling that something might be off, but a specific, timestamped pattern match from your own data that your doctor can evaluate.
Sentinel evaluates your data every single time you log a reading. There is no scheduled scan, no overnight batch — the moment you tap Save on a BP entry, Sentinel immediately checks all three streams against your personal baseline and updates its pattern status.
This means Sentinel can only protect you for the moments you log. A drop in blood pressure, a shift in heart rate, a narrowing pulse pressure — if it happens between readings and is never logged, Sentinel cannot see it. Consistent logging is not just good habit — it is what gives Sentinel the data it needs to work.
Sentinel is your first line — but it only speaks when you give it something to say.
Ask is a conversational analytical helper built into CardiacLens. It knows every feature of the app and has full awareness of your logged data across all streams — BP, HR, fluid, meals, symptoms, notes, medications, and activities — so you can ask plain questions and get data-driven answers based on what you have actually recorded.
Cross-stream pattern analysis — Ask looks across all your data simultaneously:
About your data — trends, averages, and specific readings:
About how the app works:
Tap the 💬 Ask button on the main screen. A conversation panel opens. Type your question in plain English — no special format required.
Ask reads your entire logged history — BP readings with HR, fluid entries with notes, weight, symptoms with their notes, medications, meals, activities, and standalone notes — and gives you a data-driven answer. Notes attached to any log entry are fully included, so the context you wrote when logging a symptom or a fluid entry is visible to Ask when analyzing patterns.
For pattern questions, Ask examines multiple data streams simultaneously — looking at what was logged before, during, and after events to surface temporal correlations and note text patterns. Findings are always framed as data observations: “The data shows…”, “Of your X readings…”, “Your notes frequently mention…”
Conversation persistence. Your session is saved automatically when you close the panel. When you reopen Ask within 48 hours, a blue banner at the top shows when the session was from and offers a Start Fresh button if you want to begin a new conversation instead. After 48 hours the session clears automatically.
If your question is unclear, Ask will offer you a few clearer versions to tap. You can ask follow-up questions in the same window — the conversation stays connected and builds on previous answers.
When Ask produces a significant data finding — a pattern, a correlation, a before-and-after comparison — a small 📌 Pin this finding button appears below the response.
Tapping it saves the full response as a Pinned Event with today’s date, the raw data context from that day, and the complete text of Ask’s analysis. The pin is immediately available in Pinned Events and appears in your Doctor’s Report — ready to bring to your next appointment.
The button changes to ✅ Pinned to confirm. You decide which findings are worth keeping — pin as many or as few as you choose.
Ask will: observe patterns in your data and report what it finds. “Of your 17 HR readings above 75, 82% occurred between 5:30 PM and 10:30 PM” is a data observation. “Your cough notes mention lying down in 14 entries” is a data observation. Ask is designed to surface these findings clearly and present them in a form you can bring to your cardiologist.
Ask will not: interpret what those patterns mean for your health, suggest a diagnosis, recommend changing medications, or replace your care team. When a finding crosses from data observation into clinical meaning, Ask will say: “That pattern is worth noting — it would make a good Pinned Event for your next appointment.”
The distinction is intentional. Ask gives you the evidence. Your cardiologist gives you the interpretation. Both are necessary. Neither replaces the other.
✅ Ask about correlations. The most powerful questions are cross-stream: “What was happening in the hours before my elevated HR readings?” “Does my fluid intake affect my afternoon BP?” Ask can look across all your data simultaneously.
✅ Ask about your notes text. Every note you wrote when logging a symptom, fluid entry, meal, or activity is available to Ask. “Look at my cough notes and find any repeated words or situations” is a powerful question. Your own observations become searchable patterns.
✅ Ask about a specific date. “Tell me everything logged on April 1 and what happened in the 3 hours before my chest pain entry” pulls the complete picture for that day.
✅ Ask before and after comparisons. If a medication changed, a pacemaker was adjusted, or a procedure happened — ask Ask to compare the weeks before and after. Temporal break points reveal a lot.
✅ Follow up. After an answer, keep asking in the same window. “Are most of those events in the morning?” is a valid follow-up. The conversation stays connected.
✅ Pin what matters. When Ask finds something worth bringing to your doctor, tap Pin This Finding. It lands in Pinned Events and your Doctor’s Report automatically.
✅ Start simple if new. If you are new to Ask, start with “What should I tell my cardiologist about at my next appointment?” to get a feel for what it can surface.
Tap the 🍴 tools icon on the left side of the Ask input bar. A picker opens with two options: 🥦 Food Check and 🍲 Meal Builder. Tap either one to open it. The tool takes over the full panel. Tap ← Back at any time to return to the Ask conversation thread.
Enter a food or product name, how it is prepared, how much you will actually eat, and any label values you have available. All label fields are optional — if you leave them blank, Ask uses its own nutritional knowledge for that food.
Sodium, Potassium, and Phosphorus each have a mg / %DV toggle beneath the field. Some labels show milligrams; others show % Daily Value. Switch the toggle to match what your label shows — Ask converts %DV to mg automatically using FDA Daily Values (Sodium 2,300 mg · Potassium 4,700 mg · Phosphorus 1,250 mg) before evaluating.
The preparation method matters clinically. Boiling and draining removes up to 50% of potassium from vegetables. Canning adds sodium. Drying concentrates all nutrients. The prep method dropdown is organized into four groups — Fresh/Uncooked, Wet heat, Dry heat, and Processed/Preserved — so you can quickly find the right option and Ask can account for how preparation changes the nutritional profile.
Ask evaluates the food against your logged conditions and current medication list. The response begins with one of three indicators: ✅ Generally fine for occasional use, ⚠️ Worth watching, or 🚫 High concern — discuss with your care team, followed by a brief factual summary specific to your profile.
After a Food Check result, tap the 🍴 tools icon again and select Food Check to run another check. Your last-entered values are remembered — the form reopens pre-filled so you can change just the prep method, portion, or any value and resubmit without starting over. Food Check is a nutritional awareness tool, not a dietary prescription. Always discuss dietary restrictions with your cardiologist and renal dietitian.
Meal Builder evaluates the combined nutritional load of an entire meal — not just one food. This matters because a single item may be fine in isolation but the combined sodium, potassium, and phosphorus of a full meal may exceed your safe threshold. Meal Builder is also the right tool for planning before grocery shopping — build the meal you intend to make and check it before you buy.
Building a meal: Give your meal a name and select its type (Breakfast, Lunch, Dinner, Snack, or Meal). Then add items one at a time — enter the food name, prep method, portion, and any label values. Tap + Add to Meal. Running totals for Sodium, Potassium, and Phosphorus update after each item so you can see the cumulative load building in real time.
Analyzing a meal: When all items are added, tap 🔍 Analyze Meal. Ask evaluates the combined totals against your conditions and medications and returns a single verdict for the whole meal with a 3–4 sentence summary. After the analysis, a save/modify bar appears in the conversation thread with two options.
✏️ Modify & Re-analyze: Reopens the builder with all items still loaded. Change a portion size, swap a prep method, remove an item, or add a new one — then tap Analyze again. This is also how you correct an individual item: remove it from the list and add it back with the corrected values.
💾 Save to Library: Saves the meal to your personal Saved Meals Library. A saved meal can be selected directly from the Log Meal screen — tap Log Meal and your saved meals appear at the top, organized by type, so you can pre-fill your log entry in two taps. If you opened the builder by tapping ✏️ Edit on a meal in Manage Meals, this button reads 💾 Update instead and updates that meal in place rather than creating a new one.
You can also save a meal directly from the builder before analyzing it using the 💾 Save button at the bottom of the form. This is useful when you want to save a meal template for future use without running an analysis every time.
Tap the paperclip button (📎) next to the food tool button to attach a PDF or image before sending your question. Use this for hospital discharge summaries, lab reports, specialist letters, or any clinical document you want Ask to read alongside your health data.
Example questions with attachments: "What BP target did my discharge summary specify?" · "Does this lab result explain my HR shift since March?" · "What medications were listed on this clinic note?"
After tapping 📎, a file picker opens. Select your file — a preview strip appears above the input showing the filename with a ✕ to remove it before sending. The blue dot on the 📎 button confirms a file is loaded. When you send your question, the document goes with it.
Ask focuses only on the parts of the document relevant to your specific question — it does not reproduce or summarise the full document. The response combines what it found in the document with your logged CardiacLens data to give you a grounded, contextual answer.
CardiacLens is free, source-available, and built by a cardiac patient for cardiac patients. There is no subscription, no ads, and no paywall — ever. If it helps you manage your health, a small contribution means a lot.
Your support helps cover hosting, development time, and keeps CardiacLens free for every patient who needs it.
💙 Support via PayPal100% optional — your health data stays private regardless
Secure Access locks the entire CardiacLens app behind a private combination you choose: one key letter + one color. When the lock is on, the app opens to a grid of colored tiles. Anyone who does not know your combination cannot open your health data.
Wrong sequence → the grid shakes, progress clears, and after a few failures a short cooldown starts. After 4 wrong sequences the “Forgot your code?” link appears.
Answer the recovery question you set during setup. Correct answer unlocks the app. If you used the 8-second hold, Settings also opens focused on Secure Access so you can change or disable the combination.
Limits: 3 wrong recovery answers → 5-minute lockout of the recovery panel. After that you can try again.
Go to Settings → 🔒 Secure Access.
Secure Access data stays only on this device. There is no server account. Back up your data regularly (Export ALL Data). If you forget both the combination and the recovery answer, the 8-second hold + recovery path is the emergency way back in; after three wrong recovery answers you must wait five minutes before trying again.
Your data stays private — stored locally on this device only
Pattern awareness tool only. Not a medical treatment. Does not replace your care team. Math is directional, not definitive. If you experience chest discomfort, shortness of breath, or dizziness — contact your cardiologist, do not use this tool.
Track how your medications affect your heart rate over time
Doctor's Report uses the date range selected above (or the last 30 days if none is selected) and generates immediately — independent of Generate Analysis.
Pick any date to see a focused 20-day window (14 days before → anchor → 5 days after) with all metrics and events aligned on the same timeline.
Welcome to CardiacLens.
Set up Secure Access to protect your health data. Go to Settings → 🔒 Secure Access to choose your letter and symbol.