Feature: Endocrinologist Visit Report (PDF Export)#670
Closed
greyghost99 wants to merge 14 commits into
Closed
Conversation
loopandlearn#649) * Move release flow to PR-based and add CODEOWNERS for branch protection - Add .github/CODEOWNERS - Cut a release branch in release.sh and open two PRs (sync to dev, release to main) instead of merging dev into main locally - Move tag creation into a workflow that fires on push to main - Skip auto_version_dev when Config.xcconfig was changed in this push, so merging the release sync-PR into dev does not double-bump * Make workflow guards portable via fork check Replace the hardcoded 'loopandlearn' owner check in workflows with a fork check, so the workflows run on any non-fork repository (including a test org) while still skipping on contributor forks. * Allow skipping sister repo updates in release.sh Set SKIP_SISTER_REPOS=1 to bypass the LoopFollow_Second / LoopFollow_Third update_follower steps. Default behavior is unchanged: both sister repos are updated as today and missing directories still cause a hard error, so a forgotten clone in production fails fast. * Revert "Allow skipping sister repo updates in release.sh" This reverts commit c2792b8. * Skip patch hunks for files missing from sister repos Sister repos (LoopFollow_Second / LoopFollow_Third) are intentionally stripped of dev-only files like release.sh, auto_version_dev.yml, lint.yml, and warn_main_pr.yml. Any release patch that touches one of those files used to abort the sister-repo update with 'No such file or directory'. Now update_follower runs git apply --check first, parses the missing-file errors, and re-applies with --exclude for each, so the sister patch covers the files that actually exist. * Revert "Skip patch hunks for files missing from sister repos" This reverts commit 45b9871.
* Diagnose and skip rogue Nightscout profile records Profile fetch now uses /api/v1/profiles?count=1 with find[startDate][$lte]=now, so future-dated records can no longer block the active profile. Adds a "Run diagnostics" button in the Remote Settings Debug section that fetches 14 days of profile history and surfaces three failure modes: - Bundle ID mismatch when Loop and Trio share a Nightscout - Alternating device tokens from multiple installations - Future-dated profile records left over from a wrong-clock uploader The bouncing-tokens check compresses consecutive same-token runs and only warns on actual token alternation, not normal token rotation. * Widen bouncing-token check and surface shift history Three changes to the profile diagnostics: - Drop the 14-day find[startDate][$gte] filter. A slow A→B→A pattern spread across months only registers as one transition inside a 14-day window, so the bouncing-tokens check would silently miss it on servers that honor the filter. The existing 1000-record cap now defines the scope, which goes back as far as upload frequency allows. - Fall back to created_at when sorting profile records, so uploaders that omit startDate don't cluster at .distantPast and corrupt the run-length compression. - Include the chronological list of token shifts in the bouncing-tokens warning. Each row shows when the shift happened and the abbreviated from→to tokens, so users can see at a glance which devices are competing instead of just "3 tokens involved across N records".
…wed LA (loopandlearn#656) pendingForegroundRestart could outlive the condition that triggered it: a brief foreground entry while the renewal overlay was up latched the intent, the user backgrounded before didBecomeActive ran, the background renewal then replaced the LA, and the next foreground entry minutes later fired the deferred restart against an already-fresh LA. - adoptPushToStartActivity clears pendingForegroundRestart on every adoption — a freshly-adopted LA resolves the renewal-window condition that latched the intent. - performForegroundRestart re-checks renewalFailed / overlayShowing / pushToStartLooksStuck before tearing down; bails if none still hold. - Deferred-foreground-restart push-to-start is tagged reason="deferred-foreground-restart" via a single-shot nextStartReasonOverride, so the stale-latch event is no longer indistinguishable from a real user start in logs.
A 6.1.0 user reported the Live Activity vanishing and refusing to come back without a manual Restart. Trace: APNs returned 410 on the per- activity push token at 04:42; handleExpiredToken ended the activity but the eventual iOS .dismissed (4 h later, under the default dismissal policy) was classified as a user swipe and locked dismissedByUser=true. Root cause is two cooperating bugs around an app-initiated end(): - end() nulls `current` and clears laRenewBy. handleExpiredToken's comment said "Activity will restart on next BG refresh via refreshFromCurrentState()", but renewIfNeeded short-circuits when current is nil and performRefresh's bind-existing path rebinds to the just-ended activity. bind() then clears endingForRestart, so the late .dismissed reads as renewBy=0 / renewalFailed=false / endingForRestart= false — branch (c) "USER" in the classifier. - The classifier had no way to recognize a stale observer firing for an activity the app no longer tracks. Fixes: - handleExpiredToken drives the restart synchronously on iOS 17.2+ (attemptPushToStartCreate "expired-token"), so the orphaned post-410 state is short-lived and adoption of the fresh activity cancels the old observer. - performRefresh / update bind-existing only to activities in .active state. Binding to an .ended/.dismissed corpse would clear endingForRestart and re-attach an observer that only ever delivers .dismissed. - .dismissed classifier gains branch (d): if the dismissed activity is not the one we currently track, log and take no action — only the foreground LA can be user-swiped, so a stale-observer delivery for an already-replaced activity must not latch dismissedByUser=true.
When iOS reaches the Live Activity lifetime cap before renewal fires it delivers .ended, not .dismissed. The state observer only ran restart logic on .dismissed, so handleForeground saw renewalFailed=false and renewBy still in the future and returned "no action needed", leaving the LA dark until manual force-restart. Mark laRenewalFailed=true on the .ended path (gated on wasCurrent and !endingForRestart) so the next foreground entry triggers performForegroundRestart, which sweeps the corpse activity and pushes a fresh one.
…oopandlearn#661) Dexcom Share returns each reading twice when both the iPhone Dexcom app and the Apple Watch app upload to the same account (~9-10 s apart, same SGV). Without deduplication the two most recent entries in bgData were always identical, producing delta = 0. The NS fetch path already had inline deduplication. Extract it into a shared helper (deduplicateBGReadings) and apply it to the Dexcom-only path as well.
Contributor
|
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
✨ Feature: Endocrinologist Visit Report (PDF Export)
Summary
This PR adds a PDF report generator to LoopFollow that produces a professional, clinic-ready endocrinologist visit report directly from Nightscout data — no third-party services, no external dependencies, and no internet connection required beyond the existing Nightscout connection.
The report is inspired by the LoopInsights report format and designed to give endocrinologists a clean summary of a patient's AID performance over any chosen period.
Motivation
Patients using DIY AID systems (Loop, Trio, iAPS) often arrive at endo appointments without structured data summaries. Apps like Dexcom Clarity and Tidepool produce reports but don't have visibility into Loop/Trio-specific data. LoopFollow already fetches and displays all the relevant data — this feature simply packages it into a shareable PDF that patients can AirDrop, email, or print before their visit.
What's New
Report Contents (all sections are individually toggleable)
Report Options
Auto-Fill from Nightscout
A "Auto-Fill from Nightscout" button in the Therapy Settings section calls
/api/v1/profile/current.jsonusing the existingNightscoutUtilsinfrastructure and pre-fills CR, ISF, basal rate schedule, and target glucose — no manual re-entry needed.Files Added
LoopFollow/Stats/EndoReportGenerator.swiftCore PDF rendering engine using PDFKit + CoreGraphics. Contains
EndoReportConfig,EndoReportGenerator, per-page layout, all chart drawing, and the daily breakdown row renderer. No external dependencies.LoopFollow/Stats/EndoReportView.swiftSwiftUI sheet UI for configuring and generating the report. Contains
EndoReportView,NightscoutProfileFetcher,ShareSheet, andColor/UIColorhex extensions. All settings are persisted via@AppStorage.Files Modified
LoopFollow/Stats/AggregatedStatsView.swiftAdded
@State private var showEndoReport = false, a toolbar button (doc.richtexticon) next to the existing Refresh button, and a.sheetpresentation ofEndoReportView.Technical Notes
AGPCalculator,TIRCalculator,GRICalculator,SimpleStatsViewModel, andStatsDataServiceare used directly. No data is re-fetched.NightscoutUtils.executeRequestwith.profileevent type. Includes a lenient three-layer decoder that handles missing fields gracefully across Nightscout variants (Trio, Loop, standard).DispatchQueueand dispatches UI updates back to main.@AppStorage.Testing
Tested on iPhone simulator (iOS 26.5) with real Nightscout data. Verified:
Date range presets — all tested:
Daily breakdown pagination tested across all ranges — confirmed correct number of pages generated and all days present, newest to oldest, for each preset and custom range.
Screenshots
Report Configuration UI
Report Period & Patient Info
Devices & Therapy Settings
Therapy Settings Auto-Filled from Nightscout
Schedule Mode with Chart Preview
Section Toggles & Theme Colour
Generated PDF
Page 1 — Summary Dashboard
Page 2 — Daily Glucose Breakdown
Checklist
LoopFollow)@AppStorageStorage.shared,dateTimeUtils)NightscoutUtilsinfrastructure