Skip to content

Commit 4d19974

Browse files
marionbarkerbjorkertgithub-actions[bot]luborjurenaquarktwain
authored
Release v7.0.0 (#726)
* Move release flow to PR-based and add CODEOWNERS for branch protection (#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 c2792b8363d6057e02700663b411ca9c71aadb3d. * 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 45b9871af675f51ee49037a812c66fad8bd5717d. * CI: Bump dev version to 6.1.1 [skip ci] * Diagnose and skip rogue Nightscout profile records (#635) * 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". * CI: Bump dev version to 6.1.2 [skip ci] * Prevent stale foreground-restart latch from tearing down freshly renewed LA (#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. * CI: Bump dev version to 6.1.3 [skip ci] * Recover Live Activity after APNs 410 token expiry (#657) 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. * CI: Bump dev version to 6.1.4 [skip ci] * Auto-restart Live Activity when iOS sends .ended (#659) 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. * CI: Bump dev version to 6.1.5 [skip ci] * Deduplicate Dexcom Share readings to fix delta always showing zero (#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. * CI: Bump dev version to 6.1.6 [skip ci] * Remove storyboard, migrate to SwiftUI layout (#608) * Remove Main.storyboard and migrate to SwiftUI app lifecycle Replace UIKit storyboard/SceneDelegate architecture with SwiftUI App entry point (LoopFollowApp.swift) and TabView (MainTabView.swift). Convert MoreMenuViewController to SwiftUI (MoreMenuView.swift). Add SwiftUI wrappers for Remote and Nightscout tabs. Remove 6 obsolete UIKit wrapper view controllers and ~300 lines of tab management code from MainViewController. * Migrate info table from UITableView to SwiftUI List Replace UITableView with SwiftUI InfoTableView hosted in MainViewController. Make InfoManager an ObservableObject so data updates trigger SwiftUI rebuilds automatically. Remove UITableViewDataSource conformance and table delegate methods. No changes needed to the 10 Nightscout controller files that populate the table data. * Migrate statistics and pie chart from UIKit to SwiftUI Replace 7 UILabel properties and DGCharts PieChartView with a StatsDisplayModel ObservableObject and hosted StatsDisplayView. The pie chart uses a UIViewRepresentable wrapper for DGCharts since the Charts pod name shadows Swift Charts. Remove ~60 lines of UIKit stack layout code from MainViewController setupUI(). * Migrate BG display area from UIKit labels to SwiftUI Replace BGText, DirectionText, DeltaText, MinAgoText, serverText, LoopStatusLabel, and PredictionLabel with a SwiftUI BGDisplayView. Add pull-to-refresh via .refreshable modifier. Move loop status and prediction text updates to Observable values across DeviceStatus, DeviceStatusLoop, DeviceStatusOpenAPS, and BGData. Remove UIScrollView overlay and UIScrollViewDelegate conformance. * Migrate main layout to SwiftUI with UIKit charts embedded Replace UIStackView layout with MainHomeView SwiftUI view that composes BGDisplayView, InfoTableView, LineChartWrapper (UIViewRepresentable for DGCharts), and StatsDisplayView. MainViewController now hosts a single UIHostingController instead of managing individual UIView containers. Visibility of info table, small graph, and stats is now reactive via Storage observables in SwiftUI, removing several Combine subscriptions. BG text uses lineLimit + minimumScaleFactor instead of manual font sizing. * Clean up migration artifacts and fix post-migration bugs - Fix AVSpeechSynthesizer temporary in AppDelegate that would be deallocated before speech completes; use stored property instead - Fix appMovedToBackground tab switching to use Observable instead of dead UIKit tabBarController reference - Remove dead code: rebuildTabsIfNeeded(), updateNightscoutTabState(), traitCollectionDidChange notification relay, UIViewExtension.addBorder - Remove unused imports (Charts, UIKit, Combine) from migrated files - Remove unused synthesizer from LoopFollowApp - Remove redundant .appearanceDidChange subscription from NightscoutVC - Add missing super calls in viewWillAppear/viewDidAppear * Replace view hierarchy walking with MainViewController.shared The getMainViewController() methods in TreatmentsView, SettingsMenuView, and BackgroundRefreshManager tried to find MainViewController by casting rootViewController as UITabBarController, which always fails with the SwiftUI lifecycle. Add a weak static shared reference set during viewDidLoad and use it everywhere instead. * Fix MainViewController.shared references for stats and treatments Pass MainViewController.shared instead of nil when creating AggregatedStatsContentView in MainTabView and MoreMenuView. Replace view-hierarchy-walking getMainViewController() in TreatmentsViewModel with MainViewController.shared. * Fix info table font size to match storyboard The storyboard used system 17pt for both title and detail labels. The SwiftUI migration used .subheadline (~15pt) making text smaller. * Fix Share Logs sheet rendering blank Present UIActivityViewController via UIApplication.topMost instead of wrapping it in a SwiftUI .sheet, which rendered an empty view. * Fix back navigation from Settings sub-pages SettingsMenuView declared its own NavigationStack(path:) while already being pushed onto the outer NavigationStack from MainTabView, so sub-page back buttons popped the outer stack and jumped past Settings to Menu. Drop the nested NavigationStack and route Settings entries through the ambient stack: a single SettingsRoute enum drives a navigationDestination attached at the MoreMenuView root. The Settings entry itself becomes a NavigationLink(value:) so it doesn't compete with a navigationDestination (isPresented:) modifier, which was re-asserting Settings as the top of stack whenever a sub-page was pushed. * Harden post-storyboard migration * MainViewController is now a strong static singleton bootstrapped from LoopFollowApp.init(). Lifecycle work in viewDidLoad (Combine sinks, observers, scheduleAllTasks, migrations) runs at launch regardless of whether the Home tab is rendered, and HomeContentView reuses the singleton instead of instantiating a fresh VC each time. * MoreMenuView's eight .navigationDestination(isPresented:) modifiers are collapsed to a single MenuRoute enum routed through one .navigationDestination(for:), preventing the same destination-slot contention that previously caused Settings → Graph back navigation to jump past Settings. * MainTabView observes Storage.shared.appearanceMode so theme changes propagate; the orphaned .appearanceDidChange notification name is removed. * OPEN_APP_ACTION notification taps now dismiss any presented modal before switching to Home, matching prior SceneDelegate behavior. * Drop the unused Core Data stack (NSPersistentCloudKitContainer, saveContext) from AppDelegate, the dead AppDelegate.window property, and the legacy UIRequiredDeviceCapabilities=armv7 / UIStatusBarTintParameters keys from Info.plist. Switch AlarmSound's keyWindow access to the connected-scenes API and generalize UIApplication.topMost likewise so it works on Mac Catalyst. * Strip redundant inner NavigationView wrappers from settings sub-views pushed onto the outer NavigationStack: Graph, General, Advanced, Calendar, Contact, Dexcom, Nightscout, BackgroundRefresh, InfoDisplay, ImportExport. Drop unused onBack parameters from AlarmsContainerView and SettingsMenuView, the unused isPresentedAsModal flag from MainViewController, and the leftover debug print in ObservableValue.set. * LineChartWrapper.updateUIView now flushes the chart on SwiftUI re-render. MainViewController.deinit removes all observers, not just the custom "refresh" one. MoreMenuView caches the app version in @State instead of constructing AppVersionManager on every body re-render. HomeModalView uses NavigationStack (not deprecated NavigationView). * MoreMenuView: render tab-switch buttons in primary color Buttons in a List inherit the accent tint, so the Features rows that switch tabs appeared blue while the NavigationLink rows that push appeared white. Use .buttonStyle(.plain) to suppress the tint and drop the now-redundant .foregroundStyle(.primary) calls. * Revert MainViewController singleton bootstrap Constructing MainViewController.shared from LoopFollowApp.init() — and reusing the same VC across HomeContentView re-creations — caused tapping the BG chart to crash with `-[__NSArrayM insertObject:atIndex:]: object cannot be nil`. Bisected to the singleton+bootstrap piece of the post-storyboard hardening; the rest of that commit (programmatic UI, MoreMenuView routing, NavigationView strip-out) is retained. Restore the prior behavior: shared is a weak static set in viewDidLoad, HomeContentView constructs a fresh MainViewController each time, and the LoopFollowApp.init() bootstrap is removed. Known follow-up: lifecycle work in viewDidLoad (Combine sinks, scheduleAllTasks, migrations) again only runs when the Home view is first rendered, so a user who has moved Home off the tab bar gets degraded behavior until they navigate to it. * MoreMenuView: make tab-switch rows full-row tappable Wrap Button labels in an HStack with a trailing Spacer and contentShape so the entire row is tappable, matching the hit area of NavigationLink rows. Extract the pattern into a small FullRowButton helper, used for both tab-switch rows and Share Logs. * Align units-selection conflict resolution with integration branch Move the Diagnostics section out of Section("Speak BG") (was nested at the wrong indent), match StatsDisplayModel field order, and add the spacing line in updateStats. * MoreMenuView: fix cross-row tap routing in Features section Mixing Button and NavigationLink rows in the same List ForEach caused taps on a NavigationLink row to fire a sibling Button row's action — e.g. tapping Alarms with Stats in the tab bar would switch to the Stats tab instead of pushing the alarms detail. Make every row in the menu's List a uniform FullRowButton and drive pushes from state via .navigationDestination(isPresented:). Add an opt-in chevron to FullRowButton so navigating rows render the standard disclosure indicator. * MoreMenuView: keep Settings as a value-based NavigationLink Mixing .navigationDestination(isPresented:) with .navigationDestination(for:) on the same view shadowed the value-based SettingsRoute registration once SettingsMenuView was on the stack, so sub-rows like Units and Metrics couldn't push. Settings sits alone in its section, so it doesn't need the uniform-Button treatment used in Features and Logging — restore it to a NavigationLink and route it through the existing .navigationDestination(for:) channel. * Fix navigation between alarms and menu * Drive Before-First-Unlock recovery from AppDelegate Move BFU recovery (Storage.reloadAll) out of MainViewController and into AppDelegate so it runs even when the home tab's UIHostingController has not yet materialized — necessary under the SwiftUI App lifecycle, where a BG-only launch (BGAppRefreshTask, BLE wake, prewarming) may complete and the device may unlock without MainViewController ever being created. AppDelegate observes protectedDataDidBecomeAvailable (authoritative signal) and willEnterForeground (fallback), with a race-guard re-check immediately after observer registration. Recovery is idempotent via needsBFUReload. MainViewController now reacts to a new .bfuReloadCompleted notification by showing the loading overlay and rescheduling tasks; if it is not alive when the notification fires, its viewDidLoad will later see the already-reloaded Storage values and schedule tasks correctly on first load. * Keep MainViewController alive regardless of tab layout Make MainViewController.shared a strong, long-lived singleton created once via bootstrap() on first foreground, so the data pipeline, alarms and background audio run even when Home is moved into the Menu rather than a tab. Home views reuse the single instance instead of creating new ones, so the singleton is never displaced. Defer the one-shot BG graph zoom until the chart has a real frame and re-render the graph on every appearance, so the curve stays visible when Home is reached from the Menu or moved between tab bar and Menu. Restore the one-time telemetry consent prompt that was lost when SceneDelegate was removed, presenting it from MainTabView on first appearance for undecided installs. * Fix issues found in post-migration review - Speak BG quick action: under the SwiftUI scene lifecycle UIKit delivers Home Screen quick actions to the window scene delegate and never calls application(_:performActionFor:). Install a scene delegate via configurationForConnecting that handles warm taps, cold-launch shortcut delivery, and mirrors the Live Activity la-tap URL handling that moves with it. - Nightscout tab: show a hint instead of a blank page when no URL is configured, and recreate the web view when the URL or token changes (the page was loaded once in viewDidLoad and stayed stale until app restart). - Stats: resolve MainViewController lazily with a fallback to the shared instance, so stats work when the tab is built before bootstrap() has run (cold launch with Statistics as selected tab). - Remove the unwired NightscoutSettingsViewModel delegate chain. - Make LineChartWrapper.updateUIView a no-op; MainViewController already notifies the charts whenever it mutates their data. * Fix squished Home layout caused by phantom keyboard inset iOS sometimes replays a stale keyboard frame when the app returns to the foreground, compressing the Home screen by a keyboard's height until a rotation forces the safe area to recompute. Home has no text input, so opt out of keyboard avoidance at both hosting layers. * Suppress silent passive notifications in willPresent handler (#655) UNUserNotificationCenterDelegate.willPresent returned [.banner, .sound, .badge] unconditionally, which meant any notification iOS routed through this handler while the app was foregrounded produced sound — including the Live Activity push-to-start payload, which is intentionally silent (interruption-level: passive, empty title/body). Now returns [] for passive notifications and for ones with empty title/body. The four intentional alerts (renewal-failed, APNs credentials missing, push-to-start token missing, alarms) all use non-empty title/body and the default .active interruption level, so they continue to surface. Also expanded the willPresent log line with interruptionLevel and title/body presence so future reports can confirm whether iOS routed a given payload here. * Bump fastlane to 2.235.0 and jwt to 3.2.0 (CVE-2026-45363) (#663) * Bump fastlane to 2.235.0 and jwt to 3.2.0 (CVE-2026-45363) jwt < 3.2.0 accepts attacker-forged tokens when an empty or nil key is used with HMAC algorithms (GHSA-c32j-vqhx-rx3x). fastlane 2.233.1 pinned jwt < 3, blocking the fix. fastlane 2.235.0 relaxes that to jwt < 4, allowing the upgrade to 3.2.0. * Remove json and addressable pins no longer needed with fastlane 2.235.0 * Fix BG history truncation caused by duplicate readings from multiple uploaders (#662) * Increase Nightscout BG entry count to handle multiple uploaders Some users have multiple sources uploading BG entries to Nightscout for the same sensor (e.g. a closed-loop system plus two Dexcom platform apps), resulting in up to 3 entries per 5-minute slot. The previous count cap of days × 2 × 288 was too low in those cases, silently truncating the oldest portion of the requested time window before deduplication. Raise the multiplier from 2 to 4 in both BGData.swift and StatsDataFetcher.swift, giving headroom for up to 4 uploaders. The date[$gte] filter still bounds the window, so no extra data is returned when fewer entries exist. * Supplement Dex Share with NS data when Dex doesn't cover the full window When Dexcom credentials are present, LoopFollow fetches from Dex Share first. If the Dex data doesn't reach back to the start of the requested window (e.g. Dex Share's 24h cap when more days are configured, or any other gap), fall through to webLoadNSBGData so Nightscout fills in the older portion. The existing merge logic already handles stitching the two sources together correctly. * Deduplicate Dex Share readings before use Multiple Dexcom uploaders (e.g. G7 iPhone app + Apple Watch) each write readings to Dexcom's cloud, causing the Share API to return 2+ entries per 5-minute slot. With the API's hard cap of 288 readings, duplicates consume the budget and the returned data covers only ~15h instead of 24h. Dedup Dex Share data with the same 30-second window used for Nightscout, so both the NS-supplement path and the direct ProcessDexBGData path receive clean, deduplicated readings. * Extract BG entry-count multiplier into named globalVariables.maxExpectedUploaders * Reuse deduplicateBGReadings helper for Dexcom Share dedup * Match Trio's override/temp-target colors for non-Loop backends (#668) * Quick-Pick Boluses and Meals for remote commands (#603) * Common remote commands * Help texts * Rename Common to Quick-Pick throughout Renames all files, classes, types, and UI labels introduced by the quick-pick feature from "Common" to "Quick-Pick": - CommonBoluses/ → QuickPickBoluses/ - CommonMeals/ → QuickPickMeals/ - CommonBolusesManager → QuickPickBolusesManager - CommonMealsManager → QuickPickMealsManager - CommonSectionHeader → QuickPickSectionHeader - UI labels: "Common Boluses/Meals" → "Quick-Pick Boluses/Meals" * Add Nightscout WebSocket support for real-time data updates (#606) * Add Nightscout WebSocket (Socket.IO) support for real-time data updates Connect to Nightscout's Socket.IO endpoint to receive push notifications when new data arrives, instead of waiting for the next poll cycle. The WebSocket acts as a smart trigger: when a dataUpdate event arrives, only the relevant data types (BG, treatments, device status, profile) are fetched based on which keys are present in the delta payload. When WebSocket is connected and authenticated, polling intervals are extended (BG 3x, device status 3x, treatments 2→10 min, profile 10→30 min) so HTTP polling becomes a safety net. On disconnect, polling immediately reverts to normal intervals. The feature is always-on when Nightscout is configured — no user setting needed. A read-only connection status is shown in Nightscout settings. - Add Socket.IO-Client-Swift 16.1.1 via SPM - Add NightscoutSocketManager for connection lifecycle - Add NightscoutSocketDataHandler for selective push-trigger logic - Extend polling intervals when WebSocket is authenticated - Show WebSocket status in Nightscout settings - Wire up lifecycle in MainViewController (init, foreground, refresh) - Add staleness detection (10 min fallback to polling) * Add opt-in WebSocket toggle with info sheet and refresh on disconnect - Add webSocketEnabled storage property (default off) so users opt in - Replace read-only status with toggle + info button in Nightscout settings - Info sheet explains real-time updates, polling fallback, and battery impact - On toggle off: disconnect socket and trigger full refresh to restore normal polling intervals immediately - On unexpected socket disconnect: post refresh notification so extended polling intervals revert to normal without waiting for them to expire * Fix stale WebSocket session on config change, remove redundant staleness timer - Disconnect WebSocket when Nightscout URL/token validation fails, preventing the old session from streaming data from a previous server while polling has switched to the new config - Reorder removeAllHandlers() before disconnect() so intentional disconnects don't fire the event handler that would reconnect to an invalid URL - Remove staleness timer — the extended polling intervals (3x/5x) already serve as the safety net when WebSocket data stops flowing * Limit WebSocket to foreground and default-on Disconnect the Nightscout WebSocket when LoopFollow moves to the background and re-establish it when returning to the foreground. Polling continues to handle background updates, so the persistent connection no longer holds the cellular radio out of idle while the app is not in use. With the battery cost bounded to foreground time, the toggle now defaults to on. Inline documentation updated to describe the foreground/background behavior. * Resume Nightscout polling promptly when the WebSocket disconnects While the socket is authenticated, each REST poll is rescheduled with a multiplier on the assumption WS will publish the next reading before the poll fires. Without an explicit catch-up, those long delays carry over when the socket goes away — most visibly on background entry, where the user could sit on stale data for 10–15 minutes after the screen locked. Fire each Nightscout poll immediately on disconnect. Their actions then reschedule on the normal un-multiplied cadence, since connectionState is no longer .authenticated. Gate on the previous state so the reconnect dance in connectIfNeeded() and other no-op disconnect paths don't trigger spurious REST round-trips. * Default debug logging on and prompt for description when sharing logs (#627) Flip the debugLogLevel default to true and add a migration step so existing users with it stored as false also get it enabled, ensuring shared logs contain useful detail when reporting problems. When the user taps Share Logs, present a sheet asking for a short description of the issue. The description is written to a ShareNotice_<timestamp>.txt file (date, app version, branch+sha, user description) and included alongside the log files in the iOS share sheet. * Add optional yesterday BG comparison line to main graph (#665) Adds a Graph Settings toggle (Nightscout only) that overlays yesterday's BG curve on the main graph, time-shifted by 24h so it aligns with the same clock time today. Drawn as a thin dimmed gray line with no dots, purely for comparison. When enabled, one extra day of history is fetched and the overlay is capped to now plus the configured hours of prediction so it never extends further into the future than the prediction line. * Scale info data table text with Dynamic Type (#667) * Remove Main.storyboard and migrate to SwiftUI app lifecycle Replace UIKit storyboard/SceneDelegate architecture with SwiftUI App entry point (LoopFollowApp.swift) and TabView (MainTabView.swift). Convert MoreMenuViewController to SwiftUI (MoreMenuView.swift). Add SwiftUI wrappers for Remote and Nightscout tabs. Remove 6 obsolete UIKit wrapper view controllers and ~300 lines of tab management code from MainViewController. * Migrate info table from UITableView to SwiftUI List Replace UITableView with SwiftUI InfoTableView hosted in MainViewController. Make InfoManager an ObservableObject so data updates trigger SwiftUI rebuilds automatically. Remove UITableViewDataSource conformance and table delegate methods. No changes needed to the 10 Nightscout controller files that populate the table data. * Migrate statistics and pie chart from UIKit to SwiftUI Replace 7 UILabel properties and DGCharts PieChartView with a StatsDisplayModel ObservableObject and hosted StatsDisplayView. The pie chart uses a UIViewRepresentable wrapper for DGCharts since the Charts pod name shadows Swift Charts. Remove ~60 lines of UIKit stack layout code from MainViewController setupUI(). * Migrate BG display area from UIKit labels to SwiftUI Replace BGText, DirectionText, DeltaText, MinAgoText, serverText, LoopStatusLabel, and PredictionLabel with a SwiftUI BGDisplayView. Add pull-to-refresh via .refreshable modifier. Move loop status and prediction text updates to Observable values across DeviceStatus, DeviceStatusLoop, DeviceStatusOpenAPS, and BGData. Remove UIScrollView overlay and UIScrollViewDelegate conformance. * Migrate main layout to SwiftUI with UIKit charts embedded Replace UIStackView layout with MainHomeView SwiftUI view that composes BGDisplayView, InfoTableView, LineChartWrapper (UIViewRepresentable for DGCharts), and StatsDisplayView. MainViewController now hosts a single UIHostingController instead of managing individual UIView containers. Visibility of info table, small graph, and stats is now reactive via Storage observables in SwiftUI, removing several Combine subscriptions. BG text uses lineLimit + minimumScaleFactor instead of manual font sizing. * Clean up migration artifacts and fix post-migration bugs - Fix AVSpeechSynthesizer temporary in AppDelegate that would be deallocated before speech completes; use stored property instead - Fix appMovedToBackground tab switching to use Observable instead of dead UIKit tabBarController reference - Remove dead code: rebuildTabsIfNeeded(), updateNightscoutTabState(), traitCollectionDidChange notification relay, UIViewExtension.addBorder - Remove unused imports (Charts, UIKit, Combine) from migrated files - Remove unused synthesizer from LoopFollowApp - Remove redundant .appearanceDidChange subscription from NightscoutVC - Add missing super calls in viewWillAppear/viewDidAppear * Replace view hierarchy walking with MainViewController.shared The getMainViewController() methods in TreatmentsView, SettingsMenuView, and BackgroundRefreshManager tried to find MainViewController by casting rootViewController as UITabBarController, which always fails with the SwiftUI lifecycle. Add a weak static shared reference set during viewDidLoad and use it everywhere instead. * Fix MainViewController.shared references for stats and treatments Pass MainViewController.shared instead of nil when creating AggregatedStatsContentView in MainTabView and MoreMenuView. Replace view-hierarchy-walking getMainViewController() in TreatmentsViewModel with MainViewController.shared. * Fix info table font size to match storyboard The storyboard used system 17pt for both title and detail labels. The SwiftUI migration used .subheadline (~15pt) making text smaller. * Fix Share Logs sheet rendering blank Present UIActivityViewController via UIApplication.topMost instead of wrapping it in a SwiftUI .sheet, which rendered an empty view. * Fix back navigation from Settings sub-pages SettingsMenuView declared its own NavigationStack(path:) while already being pushed onto the outer NavigationStack from MainTabView, so sub-page back buttons popped the outer stack and jumped past Settings to Menu. Drop the nested NavigationStack and route Settings entries through the ambient stack: a single SettingsRoute enum drives a navigationDestination attached at the MoreMenuView root. The Settings entry itself becomes a NavigationLink(value:) so it doesn't compete with a navigationDestination (isPresented:) modifier, which was re-asserting Settings as the top of stack whenever a sub-page was pushed. * Harden post-storyboard migration * MainViewController is now a strong static singleton bootstrapped from LoopFollowApp.init(). Lifecycle work in viewDidLoad (Combine sinks, observers, scheduleAllTasks, migrations) runs at launch regardless of whether the Home tab is rendered, and HomeContentView reuses the singleton instead of instantiating a fresh VC each time. * MoreMenuView's eight .navigationDestination(isPresented:) modifiers are collapsed to a single MenuRoute enum routed through one .navigationDestination(for:), preventing the same destination-slot contention that previously caused Settings → Graph back navigation to jump past Settings. * MainTabView observes Storage.shared.appearanceMode so theme changes propagate; the orphaned .appearanceDidChange notification name is removed. * OPEN_APP_ACTION notification taps now dismiss any presented modal before switching to Home, matching prior SceneDelegate behavior. * Drop the unused Core Data stack (NSPersistentCloudKitContainer, saveContext) from AppDelegate, the dead AppDelegate.window property, and the legacy UIRequiredDeviceCapabilities=armv7 / UIStatusBarTintParameters keys from Info.plist. Switch AlarmSound's keyWindow access to the connected-scenes API and generalize UIApplication.topMost likewise so it works on Mac Catalyst. * Strip redundant inner NavigationView wrappers from settings sub-views pushed onto the outer NavigationStack: Graph, General, Advanced, Calendar, Contact, Dexcom, Nightscout, BackgroundRefresh, InfoDisplay, ImportExport. Drop unused onBack parameters from AlarmsContainerView and SettingsMenuView, the unused isPresentedAsModal flag from MainViewController, and the leftover debug print in ObservableValue.set. * LineChartWrapper.updateUIView now flushes the chart on SwiftUI re-render. MainViewController.deinit removes all observers, not just the custom "refresh" one. MoreMenuView caches the app version in @State instead of constructing AppVersionManager on every body re-render. HomeModalView uses NavigationStack (not deprecated NavigationView). * MoreMenuView: render tab-switch buttons in primary color Buttons in a List inherit the accent tint, so the Features rows that switch tabs appeared blue while the NavigationLink rows that push appeared white. Use .buttonStyle(.plain) to suppress the tint and drop the now-redundant .foregroundStyle(.primary) calls. * Revert MainViewController singleton bootstrap Constructing MainViewController.shared from LoopFollowApp.init() — and reusing the same VC across HomeContentView re-creations — caused tapping the BG chart to crash with `-[__NSArrayM insertObject:atIndex:]: object cannot be nil`. Bisected to the singleton+bootstrap piece of the post-storyboard hardening; the rest of that commit (programmatic UI, MoreMenuView routing, NavigationView strip-out) is retained. Restore the prior behavior: shared is a weak static set in viewDidLoad, HomeContentView constructs a fresh MainViewController each time, and the LoopFollowApp.init() bootstrap is removed. Known follow-up: lifecycle work in viewDidLoad (Combine sinks, scheduleAllTasks, migrations) again only runs when the Home view is first rendered, so a user who has moved Home off the tab bar gets degraded behavior until they navigate to it. * MoreMenuView: make tab-switch rows full-row tappable Wrap Button labels in an HStack with a trailing Spacer and contentShape so the entire row is tappable, matching the hit area of NavigationLink rows. Extract the pattern into a small FullRowButton helper, used for both tab-switch rows and Share Logs. * Align units-selection conflict resolution with integration branch Move the Diagnostics section out of Section("Speak BG") (was nested at the wrong indent), match StatsDisplayModel field order, and add the spacing line in updateStats. * MoreMenuView: fix cross-row tap routing in Features section Mixing Button and NavigationLink rows in the same List ForEach caused taps on a NavigationLink row to fire a sibling Button row's action — e.g. tapping Alarms with Stats in the tab bar would switch to the Stats tab instead of pushing the alarms detail. Make every row in the menu's List a uniform FullRowButton and drive pushes from state via .navigationDestination(isPresented:). Add an opt-in chevron to FullRowButton so navigating rows render the standard disclosure indicator. * MoreMenuView: keep Settings as a value-based NavigationLink Mixing .navigationDestination(isPresented:) with .navigationDestination(for:) on the same view shadowed the value-based SettingsRoute registration once SettingsMenuView was on the stack, so sub-rows like Units and Metrics couldn't push. Settings sits alone in its section, so it doesn't need the uniform-Button treatment used in Features and Logging — restore it to a NavigationLink and route it through the existing .navigationDestination(for:) channel. * Fix navigation between alarms and menu * Drive Before-First-Unlock recovery from AppDelegate Move BFU recovery (Storage.reloadAll) out of MainViewController and into AppDelegate so it runs even when the home tab's UIHostingController has not yet materialized — necessary under the SwiftUI App lifecycle, where a BG-only launch (BGAppRefreshTask, BLE wake, prewarming) may complete and the device may unlock without MainViewController ever being created. AppDelegate observes protectedDataDidBecomeAvailable (authoritative signal) and willEnterForeground (fallback), with a race-guard re-check immediately after observer registration. Recovery is idempotent via needsBFUReload. MainViewController now reacts to a new .bfuReloadCompleted notification by showing the loading overlay and rescheduling tasks; if it is not alive when the notification fires, its viewDidLoad will later see the already-reloaded Storage values and schedule tasks correctly on first load. * Keep MainViewController alive regardless of tab layout Make MainViewController.shared a strong, long-lived singleton created once via bootstrap() on first foreground, so the data pipeline, alarms and background audio run even when Home is moved into the Menu rather than a tab. Home views reuse the single instance instead of creating new ones, so the singleton is never displaced. Defer the one-shot BG graph zoom until the chart has a real frame and re-render the graph on every appearance, so the curve stays visible when Home is reached from the Menu or moved between tab bar and Menu. Restore the one-time telemetry consent prompt that was lost when SceneDelegate was removed, presenting it from MainTabView on first appearance for undecided installs. * Scale info data table text with Dynamic Type Drive the info table font size and row height from @ScaledMetric so the top-right info data grows and shrinks with the iPhone's text-size setting, keeping the existing 17pt/21pt look at the default size. Cap the scaling at accessibility1 so the compact top strip stays within its layout. * Use two rows if needed * Info table adjustments * CI: Bump dev version to 6.1.7 [skip ci] * ITMS-91061 - privacy manifest for Charts framework (#666) * CI: Bump dev version to 6.1.8 [skip ci] * Remove the Nightscout-based remote command path (#618) Trio dev's APNS-based remote command support has merged to main, so the Nightscout treatment-posting variant of remote commands is no longer needed. Drop the .nightscout case from RemoteType, the Nightscout remote view and controller, the unused NoRemoteView fallback, and all .nightscout switch arms in the remote settings, import/export, and device validation paths. Storage keys are reused by the TRC and LoopAPNS variants and remain untouched. Existing users with remoteType set to "Nightscout" automatically fall back to .none on next launch: StorageValue's JSONDecoder fails on the removed enum case and returns the default value. * CI: Bump dev version to 6.1.9 [skip ci] * Cap prediction cone to shortest predBG array length (#653) Fixes #637. The cone of uncertainty in updateOpenAPSPredictionDisplay() was capped at the longest predBG array (.max()), which let the band visibly deform at the tail as shorter arrays dropped out one by one. Cap at the shortest array length instead so every cone point is computed from the same set of contributing arrays. Matches Trio's ForecastSetup (Trio/Sources/Modules/Home/HomeStateModel+Setup/ForecastSetup.swift), which uses allForecastValues.map(\.count).min() and then iterates 0 ..< localMinCount. Renamed maxLength to coneLength since the variable no longer represents a max. * CI: Bump dev version to 6.1.10 [skip ci] * CI: Re-run lint when a PR's base branch changes (#676) Add 'edited' to the lint workflow's pull_request trigger, guarded so the SwiftFormat job only re-runs when the base branch actually changed. Retargeting a PR (e.g. main -> dev) fires pull_request: edited, which the default trigger types (opened, synchronize, reopened) ignore. When the new base requires the SwiftFormat status check, GitHub leaves it 'Expected - waiting for status to be reported' with no run behind it, blocking the PR indefinitely. Re-running on base change ensures the required check actually reports against the new base. * CI: Bump dev version to 6.1.11 [skip ci] * Remove empty Nightscout group from project file (#675) The Nightscout remote command path was removed in #618, deleting the only file under LoopFollow/Remote/Nightscout/. The now-empty PBXGroup was left behind, still pointing at a folder with no contents. Remove it. * CI: Bump dev version to 6.1.12 [skip ci] * update version to 6.2.0 [skip ci] * Sanitize Nightscout token to prevent WebSocket crash on launch (#688) * Sanitize Nightscout token to prevent WebSocket crash on launch Strip whitespace, newlines, and control characters from the token before storing it and before opening the WebSocket. A stray character (typically pasted in) produced an invalid percent-encoded query in Socket.IO's URL builder, which traps on iOS 26 and crashed the app on startup. Existing saved tokens are sanitized defensively at connect time. * Sanitize URL entry path, not just token, to prevent WebSocket crash * CI: Bump dev version to 6.2.1 [skip ci] * Add search to the alarms list (#694) * CI: Bump dev version to 6.2.2 [skip ci] * Add search filter to the Add Alarm sheet (#681) * CI: Bump dev version to 6.2.3 [skip ci] * Fix stale snooze state in alarm list (#678) The alarm list categorized snoozed vs. active alarms against Date() sampled at render time, but nothing re-rendered the view when a snooze expired. A lapsed snooze could stay in the Snoozed section for hours while the editor correctly showed it as not snoozed. Refresh a now snapshot on appear and on returning to the foreground, and drive all snooze comparisons through it. * CI: Bump dev version to 6.2.4 [skip ci] * Prefer structured ISF/CR from device status over reason string (#677) Ignore a placeholder 0 in the structured ISF field so the info row falls back to the profile value instead of showing a 0 enacted ISF. Read the carb ratio from the structured CR field when present, keeping the reason-string regex only as a fallback for uploaders that don't expose it. * CI: Bump dev version to 6.2.5 [skip ci] * Bump fastlane to 2.237.0 (#701) Update the pinned fastlane version from 2.235.0 to 2.237.0 and refresh the transitive dependencies in Gemfile.lock. Notable upstream changes: - addressable minimum raised to 2.9.0 (CVE-2026-35611) - excon upper bound relaxed, resolving to 1.5.0 - match: keychain import path escaping fix, fail fast on SSH auth prompts in non-interactive shells - gym: cleaner xcpretty error handling, xcbeautify preferred * CI: Bump dev version to 6.2.6 [skip ci] * Only clear the forecast when the data source changes (#707) * Only clear the forecast when the data source changes The refresh handler used to wipe the Loop and Trio forecasts on every refresh, including every websocket disconnect, which made the Loop forecast disappear and only come back on the next loop cycle. Move the clearing so it only happens when the active system actually changes: when a device status flips between Loop and Trio, and when Nightscout is turned off so there is no device status source at all, such as Dexcom only. * Rebuild the Loop forecast when it is empty The forecast was only rebuilt when the loop cycle advanced (previousLastLoopTime < lastLoopTime), so any time the prediction was cleared within the same cycle it stayed blank until the next loop cycle. Also rebuild when predictionData is empty, so the forecast is restored on the next device-status poll regardless of cycle timing. * Fall back to the loop timestamp when the pump reports no clock A pump that omits pump.clock (e.g. some Omnipod configurations) left alertLastLoopTime at 0, so the Loop forecast anchored to epoch 0 and stretched the graph across decades. Use the loop cycle timestamp as the loop time when no pump clock is present, so the forecast anchors correctly and refreshes each cycle. * CI: Bump dev version to 6.2.7 [skip ci] * Remove migration step 1 and the legacy UserDefaults framework it kept alive (#698) Step 1 shipped in v3.0.0 (2025-07-07) and its one-year window has passed. It was already unreachable: migrationStep defaults to 9, so any user without a stored migrationStep skips every step, and users who have one are at >= 1. Removing migrateStep1() also retires its exclusive dependencies: the UserDefaultsValue / ObservableUserDefaultsValue stack, UserDefaultsValueGroups, ObservableUserDefaults, ObservationToken, AnyConvertible, and the orphaned HKQuantity+AnyConvertible. Nothing in Swift reads the app-group UserDefaults suite anymore, so AppConstants.APP_GROUP_ID goes too. * CI: Bump dev version to 6.2.8 [skip ci] * Add Nightscout database size info row and alarm (#697) Nightscout's dbsize plugin exposes the Mongo data + index size at /api/v2/properties/dbsize. Poll it on a new 6 hour scheduler task and surface it two ways: - An optional "DB Size" info row showing used MiB and the percentage of the site's configured DBSIZE_MAX. - A new "Nightscout Database Size" alarm that fires at or above a percentage threshold, defaulting to 75% to match the urgent level of the plugin itself. The database fills over weeks, so the alarm defaults to day-only and never repeats at night. Nightscout still reports the property with a zero size when db.stats() fails, which would read as an empty database rather than a missing reading. Treat that as no reading, mirroring how the plugin's own pill hides itself in that case. * CI: Bump dev version to 6.2.9 [skip ci] * Migrate charts to Swift Charts (#704) * Migrate charts to Swift Charts Replaces the danielgindi Charts and SwiftAlgorithms CocoaPods with native Swift Charts. - Rebuilds the BG graph, treatments, and prediction cone on Swift Charts with a custom pan/zoom/inspect interaction layer. - Ports the stats views (AGP, TIR, GRI) off the old Charts dependency. - Drops the Charts and SwiftAlgorithms pods and their generated support files. Chart appearance and behaviour are kept at parity with the previous implementation. * Raise app and Live Activity extension deployment target to iOS 18.0 * Show every reading and treatment under the chart scrubber When the detail selector is dragged across the chart, the pill now lists every item near the finger — the nearest glucose reading plus any treatments whose symbols fall within a screen-space capture band — instead of a single winner chosen by time. Every glucose reading stays inspectable, including where a bolus, carb entry, or an on-trace treatment (BG check, note, suspend, resume, sensor start) shares its time. * Enhance AGP, GRI, and TIR graph views with threshold indicators and improved padding * Refactor threshold label handling in AGPGraphView to use Localizer for unit display * Add label support to BandRect, enhance band label rendering in BGChartView and fix upper domain for Y label * Show Trio override names on the graph Trio uploads overrides as Exercise events with the name in the notes field and no reason field, so the graph band label fell back to the generic "Override". Resolve the name notes-first (falling back to reason for Loop) when building the override graph data, matching the info table and treatments list. --------- Co-authored-by: codebymini <daniel@codebymini.se> Co-authored-by: Daniel Mini Johansson <42831533+codebymini@users.noreply.github.com> * CI: Bump dev version to 6.2.10 [skip ci] * release.sh: verify latest script before running (#682) Guard the release flow against running an outdated copy of release.sh. Before any git operations, fetch origin/dev and compare the running script against origin/dev:release.sh, aborting if they differ. * CI: Bump dev version to 6.2.11 [skip ci] * release.sh: set gh pr repo explicitly (#685) Derive owner/repo from the origin remote and pass --repo to both gh pr create calls so the release flow does not depend on gh's inferred default repo, which fails in clones with multiple remotes. * CI: Bump dev version to 6.2.12 [skip ci] * release.sh: drop [skip ci] from version bump; allow manual tag_on_main (#686) The [skip ci] on the version-bump commit skipped the required SwiftFormat lint check (blocking the release PRs) and the tag_on_main workflow (no auto-tag). Remove it; auto_version_dev already prevents re-bumping via its Config.xcconfig guard. Also add a workflow_dispatch trigger to tag_on_main so the tag can be created manually if a run is ever skipped. * Add directional/charging skip toggles to Low, High and Battery alarms (#692) * Add option to skip low BG alarm while rising The low alarm can now stay silent while BG is climbing, firing only when the latest reading is flat or still falling. Off by default, so existing alarms are unaffected. * Add skip-if-falling for high alarm and skip-while-charging for battery The high alarm can now stay silent while BG is falling (mirror of the low alarm's skip-if-rising). The phone-battery alarm can stay silent while the phone is charging, using the uploader's charging status from Nightscout. Both are off by default. * CI: Bump dev version to 6.2.13 [skip ci] * Mark scheduled runs with nothing to build as cancelled instead of successful (#705) * Mark scheduled runs with nothing to build as cancelled instead of successful * update testflight.md, remove alive references, update loopdocs links --------- Co-authored-by: marionbarker <marionbarker@earthlink.net> * CI: Bump dev version to 6.2.14 [skip ci] * Use NightscoutUtils.constructURL for the Nightscout web viewer URL (#712) * CI: Bump dev version to 6.2.15 [skip ci] * Add fallbacks for missing TDD and timestamp in devicestatus (#709) * Fix TDD and Updated dropping out on Trio SMB follow-up uploads * Remove comments * CI: Bump dev version to 6.2.16 [skip ci] * Format Rec. Bolus with the pump's bolus increment (#700) * Format Loop's recommended bolus with InsulinMetric The Loop device status parser formatted Rec. Bolus with a hardcoded String(format: "%.2fU"), while the OpenAPS/Trio parser routed the value through InsulinMetric. The same recommendation rendered as "0.00U" on a Loop URL and "0" on a Trio URL. Use InsulinMetric on both paths. * Format Rec. Bolus with the pump's bolus increment Rec. Bolus was formatted two different ways: the Loop parser used a hardcoded String(format: "%.2fU"), while the OpenAPS/Trio parser went through InsulinMetric, which drops to one fraction digit below 10 U. The same recommendation rendered as "0.00U" on a Loop URL and "0" on a Trio URL. Both parsers now use InsulinFormatter, which derives its fraction digits from the pump's reported bolusIncrement, so the value is shown at the precision the pump can actually deliver. The unit suffix is dropped to match the other insulin rows in the info table. Clear the info row and deviceRecBolus when the device status carries no recommendation, so a stale value can't linger in the info table, the Rec. Bolus alarm condition, or the Live Activity. * CI: Bump dev version to 6.2.17 [skip ci] * Remove pre-iOS 18 Live Activity code paths (#711) * Remove pre-iOS 18 Live Activity code paths With the deployment target now at iOS 18, delete the legacy fallbacks and availability guards that only ran on older systems: - LiveActivityManager: remove startIfNeededLegacy and attemptLegacyRenewal (the Activity.request() start/renewal paths), unwrap the iOS 17.2 push-to-start guards, and drop the now-redundant @available attributes. - LoopFollowLiveActivity: collapse the dual ActivityConfiguration into the family-adaptive layout and inline contentMargins. - StorageCurrentGlucoseStateProvider: the renewal overlay is driven solely by laRenewalFailed. - EKEventStore: request full calendar access directly. - RestartLiveActivityIntent: drop the redundant iOS 16.4 availability attributes. Also delete the orphaned duplicate LoopFollow/LiveActivity/RestartLiveActivityIntent.swift, which was not referenced by any target and shadowed the maintained root copy. * Tighten Live Activity comments to describe end state * CI: Bump dev version to 6.2.18 [skip ci] * Gate startup on storage readiness for Before-First-Unlock recovery (#710) * CI: Bump dev version to 6.2.19 [skip ci] * Add custom alarm sounds (#699) * Add custom alarm sounds Users can now import their own audio for alarm tones via the tone picker (document picker, `.audio` UTType) or by dropping files into the app's Documents folder from the Files app. Imported files are copied into `Documents/CustomSounds/`, validated with AVAudioPlayer, and capped at 2 MB / 30 s. SoundFile is now an enum with `.builtin(String)` and `.custom(UUID)` cases. Built-in values encode as a bare string so existing stored alarms keep loading; custom values encode as a keyed object. Static aliases (`.indeed`, `.wrongAnswer`, …) preserve all existing call sites. AlarmSound.setSoundFile now resolves built-in via the bundle and custom via CustomSoundStore, and falls back to the default built-in with a log when a file is missing (deleted custom sound, etc.) instead of crashing. * Harden custom alarm sound imports Validate files dropped into Documents in place before moving them into the store, applying the same size, duration, and format checks as the in-app importer. This prevents a user's file from being deleted when it fails the audio check and stops oversized files from bypassing the caps. Reset the alarm's sound to the fallback built-in when the selected custom sound is deleted, and avoid rewriting the index when nothing changed. * Prevent duplicate custom sound when importing from shared folder Move the picked file into the store instead of copying when it already lives in the app's shared Documents folder, so absorbDroppedFiles() doesn't re-ingest the leftover original as a second entry. Validate before the file operation so a rejected import never deletes the user's original. * CI: Bump dev version to 6.2.20 [skip ci] * Add dismissable top banner for Nightscout, Dexcom, and heartbeat issues (#696) * Add dismissable top banner for Nightscout and Dexcom errors Failed BG downloads were only visible in the log viewer, and a failed Dexcom Share login silently fell back to Nightscout. Users with a bad token, mistyped URL, or wrong Dexcom credentials saw no data with no explanation. Add a banner above the tab view (visible on every tab, pushes content down) that surfaces these failures with a clear cause: - Nightscout fetch failures trigger a rate-limited status.json probe to classify the error (invalid token, token required, site not found, no network) using the existing NightscoutError messages. - Dexcom Share errors are mapped to friendly text (account not found, incorrect username or password, too many attempts), including the current AccountPasswordInvalid code alongside the legacy SSO_ ones. The previously silent Nightscout fallback is now shown as a warning. - Banners clear automatically on a successful fetch or when the source is deconfigured. Dismissing a banner suppresses that exact error for 30 minutes; a different error shows immediately. - Repeated failing retries reuse the same message so the banner does not re-animate every cycle. Also fix verifyURLAndToken transport-error classification, which reported DNS/connection failures as siteNotFound and everything else as networkError; it now distinguishes them by URLError code, which also improves the settings status label. * Add banner warning for frequent Bluetooth heartbeat dropouts * CI: Bump dev version to 6.2.21 [skip ci] * Add search to the Menu (#695) * Add search to the Menu covering Settings sub-items Make SettingsRoute the single source of truth for the Settings list by moving each row's title, icon and search keywords onto the route, and grouping them via menuSections(nightscoutConfigured:). SettingsMenuView now renders from that data. Add .searchable to the Menu. Search results are assembled from the same sources that build the menu — the Settings routes, the tab features and the static rows — so they stay in sync automatically. A settings result deep-links straight into its sub-screen; features, log and support-link results reuse the menu's existing navigation. * Make bottom-level settings findable in Menu search Add a leaves list to SettingsRoute describing the individual settings inside each sub-screen (row titles plus search synonyms), and include them in the Menu search index after the screen-level items. A leaf result shows a Settings → <screen> subtitle and opens the screen that contains the setting, e.g. searching "basal" finds Graph Basal under Advanced. Info Display leaves are derived from InfoType so they follow the app's info rows automatically. NavigationRow gains an optional subtitle to render these results. * CI: Bump dev version to 6.2.22 [skip ci] * Add color thresholds to the Info Display (#693) * Add color thresholds to the Info Display Individual info rows (IOB, COB, battery, pump battery, TDD, rec bolus, carbs, and sensor/cannula/insulin ages) can now turn yellow or red when their value crosses user-set thresholds — a visual cue only, with no alarm. The concerning direction is fixed per metric (battery colors when low, ages when high) and in-range values read green. Replaces the parallel infoSort/infoVisible arrays with a single ordered [InfoDisplayItem] store (migrated via migrateStep10), and reworks the Info Display settings into drill-in rows that reorder and open a per-row detail at the same time. * Use steppers for Info Display color thresholds, and color the pump row Threshold entry now uses steppers with a per-type range and step, mirroring the alarm editors, so values that need decimals (IOB, rec. bolus, the ages) accept them. The old text fields re-parsed on every keystroke and swallowed the decimal separator, making fractional thresholds impossible to enter. Direction, unit, bounds and defaults move into a single InfoColorConfig per type, so isColorable is simply "has a config". The pump reservoir row is now colorable, low-is-concerning. Pumps that only report "50+" count as exactly 50, matching what the reservoir alarm already does with latestPumpVolume. * CI: Bump dev version to 6.2.23 [skip ci] * Add new section to export APNS setting (#714) * CI: Bump dev version to 6.2.24 [skip ci] * release.sh: use merge-commit for release PRs to preserve release notes (#687) * release.sh: use merge-commit for release PRs to preserve notes The PR-based flow recommended rebase-merge, which rewrites the feature-PR commits to new SHAs on main. That breaks GitHub's auto-generated release notes (only the release author is credited, no PR list) and diverges main from dev. Switch the guidance to 'Create a merge commit' so main retains the original commits and the release notes generate correctly. * release.sh: trim rationale from release PR body * CI: Bump dev version to 6.2.25 [skip ci] * Bug fix: carb dots overlapping SMB dots on the chart (#650) - Restores pre-PR #305 behavior; no other dot positioning changes - Carb dots entered alongside an SMB were rendering at the same Y position as the SMB triangle, visually overlapping - The carb-vs-bolus collision logic was supposed to lift the carb dot above the bolus to BG+70, but only checked for Boluses (not SMB) - Regression comes from PR #305 (SMB display differentiated from boluses), which split SMBs into their own smbData array. - After the split, findNearestBolusbyTime in processNSCarbs no longer saw SMBs as "nearby treatments," so carb-near-SMB fell through to the default BG+20 offset: the same Y as the SMB itself. - processNSCarbs now also looks up smbData. - A carb within 300s of either a bolus OR SMB lifts to BG+70; otherwise BG+20. * Migrate main LoopFollow target to a synchronized folder group (#702) Convert the main LoopFollow/ group from explicit file enumeration to a PBXFileSystemSynchronizedRootGroup so files are picked up from disk. This shrinks project.pbxproj from ~2900 to ~880 lines and removes it as a source of merge conflicts when files are added, moved, or deleted. Shared LiveActivity/GlucoseConversion sources that also build into the Live Activity extension are moved to a new Shared/ folder referenced by both targets. Info.plist is kept out of the copy phase via a membership exception. Also remove dead code that was on disk but never in the build: a stale duplicate RestartLiveActivityIntent, four unreferenced orphan files, and a leftover Core Data model unused since the initial commit. * CI: Bump dev version to 6.2.26 [skip ci] * Fix Mac Catalyst build: guard liveActivity keywords case (#715) * CI: Bump dev version to 6.2.27 [skip ci] * Remove unneeded UIKit imports (#713) * Remove unneeded UIKit imports * Remove duplicate UIKit import in SettingsMenuView * CI: Bump dev version to 6.2.28 [skip ci] * Parameterize bundle ID and build workflow for identical sister-repo code (#690) * Derive Fastlane bundle identifiers from app_suffix Read app_suffix from LoopFollowDisplayNameConfig.xcconfig in the Fastfile and build the app and Live Activity extension identifiers from it, replacing the hardcoded com.<team>.LoopFollow strings. The same Fastfile now produces the correct bundle identifiers in LoopFollow, LoopFollow_Second and LoopFollow_Third without per-repo edits. Update release.sh so the conflict notice no longer lists the Fastfile, which is now identical across the sister repositories. * Make swiftformat and build workflow identical across sister repos Accept any LoopFollow* directory in Scripts/swiftformat.sh instead of only the exact 'LoopFollow' folder, so formatting runs consistently in LoopFollow, LoopFollow_Second and LoopFollow_Third. Drive the variant name in build_LoopFollow.yml's not-configured summary from ${{ github.event.repository.name }} / ${{ env.UPSTREAM_REPO }} and drop the variant name from two comments, so the workflow no longer hardcodes 'LoopFollow' in cosmetic text. The only intentional per-repo differences left are the cron minute and UPSTREAM_REPO. * Keep swiftformat folder guard intact The exact-'LoopFollow' folder check is intentional: it prevents swiftformat from reformatting source files when the tree is checked out as a sister repo (LoopFollow_Second / LoopFollow_Third), which would create spurious diffs. The script stays identical across all three repos and skips by folder name at runtime. * Sync sister repos by mirroring instead of patching Replace the git diff / git apply patch flow in release.sh's update_follower with an rsync --delete mirror of the release tree into each sister repo, followed by re-applying that instance's overlay (app_suffix, display_name, UPSTREAM_REPO, cron) and renaming the workspace. The per-instance values are derived from the sister directory name; only the staggered cron minute is passed in. Mirroring makes each sister an exact copy of the release tree except for the overla…
1 parent 9d2cea6 commit 4d19974

380 files changed

Lines changed: 9908 additions & 44750 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/build_LoopFollow.yml

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -131,10 +131,10 @@ jobs:
131131
if: steps.workflow-permission.outputs.has_permission != 'true'
132132
run: |
133133
echo "### :calendar: Scheduled Sync and Build Disabled :mobile_phone_off:" >> $GITHUB_STEP_SUMMARY
134-
echo "You have not yet configured the scheduled sync and build for LoopFollow's browser build." >> $GITHUB_STEP_SUMMARY
135-
echo "Synchronizing your fork of <code>LoopFollow</code> with the upstream repository <code>loopandlearn/LoopFollow</code> will be skipped." >> $GITHUB_STEP_SUMMARY
136-
echo "If you want to enable automatic builds and updates for your LoopFollow, please follow the instructions \
137-
under the following path <code>LoopFollow/fastlane/testflight.md</code>." >> $GITHUB_STEP_SUMMARY
134+
echo "You have not yet configured the scheduled sync and build for ${{ github.event.repository.name }}'s browser build." >> $GITHUB_STEP_SUMMARY
135+
echo "Synchronizing your fork of <code>${{ github.event.repository.name }}</code> with the upstream repository <code>${{ env.UPSTREAM_REPO }}</code> will be skipped." >> $GITHUB_STEP_SUMMARY
136+
echo "If you want to enable automatic builds and updates for your ${{ github.event.repository.name }}, please follow the instructions \
137+
under the following path <code>${{ github.event.repository.name }}/fastlane/testflight.md</code>." >> $GITHUB_STEP_SUMMARY
138138
139139
# Set a logic flag if this is the second instance of this day-of-week in this month
140140
- name: Check if this is the second time this day-of-week happens this month
@@ -148,6 +148,30 @@ jobs:
148148
echo "is_second_instance=false" >> "$GITHUB_OUTPUT"
149149
fi
150150
151+
# When a scheduled run finds nothing new to build, cancel the run so it shows
152+
# as "cancelled" instead of "success" — a green check should always mean a
153+
# new build was made.
154+
cancel_when_no_build:
155+
needs: check_status
156+
name: Cancel run when there is nothing to build
157+
runs-on: ubuntu-latest
158+
permissions:
159+
actions: write
160+
if: |
161+
github.event_name == 'schedule' &&
162+
!(vars.SCHEDULED_BUILD != 'false' && needs.check_status.outputs.IS_SECOND_IN_MONTH == 'true') &&
163+
!(vars.SCHEDULED_SYNC != 'false' && needs.check_status.outputs.NEW_COMMITS == 'true')
164+
steps:
165+
- name: Cancel the run
166+
env:
167+
GH_TOKEN: ${{ github.token }}
168+
run: |
169+
echo "Repository is up to date and no monthly build is due — cancelling this run so it is not reported as a successful build." >> "$GITHUB_STEP_SUMMARY"
170+
gh run cancel ${{ github.run_id }} --repo ${{ github.repository }}
171+
# Keep the job alive until the cancellation lands, so the run ends as
172+
# "cancelled" rather than completing successfully first.
173+
sleep 300
174+
151175
# Checks if Distribution certificate is present and valid, optionally nukes and
152176
# creates new certs if the repository variable ENABLE_NUKE_CERTS == 'true'
153177
# only run if a build is planned
@@ -161,7 +185,7 @@ jobs:
161185
(vars.SCHEDULED_BUILD != 'false' && needs.check_status.outputs.IS_SECOND_IN_MONTH == 'true') ||
162186
(vars.SCHEDULED_SYNC != 'false' && needs.check_status.outputs.NEW_COMMITS == 'true' )
163187
164-
# Builds LoopFollow
188+
# Builds the app
165189
build:
166190
name: Build
167191
needs: [check_certs, check_status]
@@ -203,7 +227,7 @@ jobs:
203227
- name: Sync clock
204228
run: sudo sntp -sS time.windows.com
205229

206-
# Build signed LoopFollow IPA file
230+
# Build signed IPA file
207231
- name: Fastlane Build & Archive
208232
run: bundle exec fastlane build_LoopFollow
209233
env:

.github/workflows/tag_on_main.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@ on:
1717
push:
1818
branches:
1919
- main
20+
# Manual fallback: if the push-triggered run was ever skipped (e.g. a commit
21+
# with [skip ci] landing on main), the tag can be created by dispatching this
22+
# workflow against main. It is idempotent — it skips if the tag already exists.
23+
workflow_dispatch:
2024

2125
jobs:
2226
tag:

Config.xcconfig

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,4 @@
66
unique_id = ${DEVELOPMENT_TEAM}
77

88
//Version (DEFAULT)
9-
LOOP_FOLLOW_MARKETING_VERSION = 6.2.0
9+
LOOP_FOLLOW_MARKETING_VERSION = 7.0.0

Gemfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
11
source "https://rubygems.org"
2-
gem "fastlane", "2.235.0"
2+
gem "fastlane", "2.237.0"

Gemfile.lock

Lines changed: 29 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -8,19 +8,19 @@ GEM
88
artifactory (3.0.17)
99
atomos (0.1.3)
1010
aws-eventstream (1.4.0)
11-
aws-partitions (1.1254.0)
12-
aws-sdk-core (3.250.0)
11+
aws-partitions (1.1267.0)
12+
aws-sdk-core (3.253.0)
1313
aws-eventstream (~> 1, >= 1.3.0)
1414
aws-partitions (~> 1, >= 1.992.0)
1515
aws-sigv4 (~> 1.9)
1616
base64
1717
bigdecimal
1818
jmespath (~> 1, >= 1.6.1)
1919
logger
20-
aws-sdk-kms (1.128.0)
20+
aws-sdk-kms (1.129.0)
2121
aws-sdk-core (~> 3, >= 3.248.0)
2222
aws-sigv4 (~> 1.5)
23-
aws-sdk-s3 (1.224.0)
23+
aws-sdk-s3 (1.226.0)
2424
aws-sdk-core (~> 3, >= 3.248.0)
2525
aws-sdk-kms (~> 1)
2626
aws-sigv4 (~> 1.5)
@@ -42,8 +42,9 @@ GEM
4242
domain_name (0.6.20240107)
4343
dotenv (2.8.1)
4444
emoji_regex (3.2.3)
45-
excon (0.112.0)
46-
faraday (1.10.5)
45+
excon (1.5.0)
46+
logger
47+
faraday (1.10.6)
4748
faraday-em_http (~> 1.0)
4849
faraday-em_synchrony (~> 1.0)
4950
faraday-excon (~> 1.1)
@@ -72,10 +73,10 @@ GEM
7273
faraday_middleware (1.2.1)
7374
faraday (~> 1.0)
7475
fastimage (2.4.1)
75-
fastlane (2.235.0)
76+
fastlane (2.237.0)
7677
CFPropertyList (>= 2.3, < 5.0.0)
7778
abbrev (~> 0.1)
78-
addressable (>= 2.8, < 3.0.0)
79+
addressable (>= 2.9.0, < 3.0.0)
7980
artifactory (~> 3.0)
8081
aws-sdk-s3 (~> 1.197)
8182
babosa (>= 1.0.3, < 2.0.0)
@@ -87,7 +88,7 @@ GEM
8788
csv (~> 3.3)
8889
dotenv (>= 2.1.1, < 3.0.0)
8990
emoji_regex (>= 0.1, < 4.0)
90-
excon (>= 0.71.0, < 1.0.0)
91+
excon (>= 0.71.0, < 2.0.0)
9192
faraday (~> 1.0)
9293
faraday-cookie_jar (~> 0.0.6)
9394
faraday_middleware (~> 1.0)
@@ -101,9 +102,10 @@ GEM
101102
highline (~> 2.0)
102103
http-cookie (~> 1.0.5)
103104
json (< 3.0.0)
104-
jwt (>= 2.1.0, < 4)
105+
jwt (>= 2.10.3, < 4)
105106
logger (>= 1.6, < 2.0)
106107
mini_magick (>= 4.9.4, < 5.0.0)
108+
multi_json (~> 1.12)
107109
multipart-post (>= 2.0.0, < 3.0.0)
108110
mutex_m (~> 0.3)
109111
naturally (~> 2.2)
@@ -124,7 +126,7 @@ GEM
124126
xcpretty-travis-formatter (>= 0.0.3, < 2.0.0)
125127
fastlane-sirp (1.1.0)
126128
gh_inspector (1.1.3)
127-
google-apis-androidpublisher_v3 (0.101.0)
129+
google-apis-androidpublisher_v3 (0.104.0)
128130
google-apis-core (>= 0.15.0, < 2.a)
129131
google-apis-core (0.18.0)
130132
addressable (~> 2.5, >= 2.5.1)
@@ -134,20 +136,20 @@ GEM
134136
mutex_m
135137
representable (~> 3.0)
136138
retriable (>= 2.0, < 4.a)
137-
google-apis-iamcredentials_v1 (0.27.0)
139+
google-apis-iamcredentials_v1 (0.28.0)
138140
google-apis-core (>= 0.15.0, < 2.a)
139-
google-apis-playcustomapp_v1 (0.17.0)
141+
google-apis-playcustomapp_v1 (0.18.0)
140142
google-apis-core (>= 0.15.0, < 2.a)
141-
google-apis-storage_v1 (0.62.0)
143+
google-apis-storage_v1 (0.64.0)
142144
google-apis-core (>= 0.15.0, < 2.a)
143-
google-cloud-core (1.8.0)
145+
google-cloud-core (1.9.0)
144146
google-cloud-env (>= 1.0, < 3.a)
145147
google-cloud-errors (~> 1.0)
146148
google-cloud-env (2.2.2)
147149
base64 (~> 0.2)
148150
faraday (>= 1.0, < 3.a)
149-
google-cloud-errors (1.6.0)
150-
google-cloud-storage (1.60.0)
151+
google-cloud-errors (1.7.0)
152+
google-cloud-storage (1.62.0)
151153
addressable (~> 2.8)
152154
digest-crc (~> 0.4)
153155
google-apis-core (>= 0.18, < 2)
@@ -157,21 +159,21 @@ GEM
157159
googleauth (~> 1.9)
158160
mini_mime (~> 1.0)
159161
google-logging-utils (0.2.0)
160-
googleauth (1.16.2)
162+
googleauth (1.17.1)
161163
faraday (>= 1.0, < 3.a)
162164
google-cloud-env (~> 2.2)
163165
google-logging-utils (~> 0.1)
164166
jwt (>= 1.4, < 4.0)
165-
multi_json (~> 1.11)
166167
os (>= 0.9, < 2.0)
168+
pstore (~> 0.1)
167169
signet (>= 0.16, < 2.a)
168170
highline (2.0.3)
169171
http-cookie (1.0.8)
170172
domain_name (~> 0.5)
171173
httpclient (2.9.0)
172174
mutex_m
173175
jmespath (1.6.2)
174-
json (2.19.7)
176+
json (2.20.0)
175177
jwt (3.2.0)
176178
base64
177179
logger (1.7.0)
@@ -182,11 +184,12 @@ GEM
182184
mutex_m (0.3.0)
183185
nanaimo (0.4.0)
184186
naturally (2.3.0)
185-
nkf (0.2.0)
187+
nkf (0.3.0)
186188
optparse (0.8.1)
187189
os (1.1.4)
188190
ostruct (0.6.3)
189191
plist (3.7.2)
192+
pstore (0.2.1)
190193
public_suffix (7.0.5)
191194
rake (13.4.2)
192195
representable (3.2.0)
@@ -199,11 +202,10 @@ GEM
199202
ruby2_keywords (0.0.5)
200203
rubyzip (2.4.1)
201204
security (0.1.5)
202-
signet (0.21.0)
205+
signet (0.22.0)
203206
addressable (~> 2.8)
204207
faraday (>= 0.17.5, < 3.a)
205208
jwt (>= 1.5, < 4.0)
206-
multi_json (~> 1.10)
207209
simctl (1.6.10)
208210
CFPropertyList
209211
naturally
@@ -218,12 +220,14 @@ GEM
218220
uber (0.1.0)
219221
unicode-display_width (2.6.0)
220222
word_wrap (1.0.0)
221-
xcodeproj (1.27.0)
223+
xcodeproj (1.28.1)
222224
CFPropertyList (>= 2.3.3, < 4.0)
223225
atomos (~> 0.1.3)
226+
base64
224227
claide (>= 1.0.2, < 2.0)
225228
colored2 (~> 3.1)
226229
nanaimo (~> 0.4.0)
230+
nkf
227231
rexml (>= 3.3.6, < 4.0)
228232
xcpretty (0.4.1)
229233
rouge (~> 3.28.0)
@@ -235,7 +239,7 @@ PLATFORMS
235239
ruby
236240

237241
DEPENDENCIES
238-
fastlane (= 2.235.0)
242+
fastlane (= 2.237.0)
239243

240244
BUNDLED WITH
241245
4.0.12

0 commit comments

Comments
 (0)