diff --git a/Localization/Localizable.xcstrings b/Localization/Localizable.xcstrings index 8ed9232..7a005bc 100644 --- a/Localization/Localizable.xcstrings +++ b/Localization/Localizable.xcstrings @@ -6489,6 +6489,9 @@ } } }, + "1 Year" : { + "comment" : "Title string for PodSessionLogRetention.days365" + }, "2 hours" : { "comment" : "Button text for 2 hour suspend duration", "extractionState" : "manual", @@ -6651,6 +6654,9 @@ } } }, + "30 Days" : { + "comment" : "Title string for PodSessionLogRetention.days30" + }, "30 minutes" : { "comment" : "Button text for 30 minute suspend duration", "extractionState" : "manual", @@ -6813,6 +6819,12 @@ } } }, + "90 Days" : { + "comment" : "Title string for PodSessionLogRetention.days90" + }, + "180 Days" : { + "comment" : "Title string for PodSessionLogRetention.days180" + }, "A new certificate will be used for the next Omnipod 5 Pod pairing." : { "comment" : "Confirmation message when a new certificate will be used" }, @@ -19477,6 +19489,9 @@ } } }, + "Each completed pod is recorded here with its fault code and ref string, if any, so you can report it to your supplier without needing a screenshot." : { + "comment" : "Footer explanation for the pod session log" + }, "Downloaded" : { "comment" : "O5 cert source: downloaded" }, @@ -24508,6 +24523,12 @@ } } }, + "Fault %1$@ · %2$@" : { + "comment" : "Format string for pod session log row summary: (1: fault code) (2: fault description)" + }, + "Fault %1$@ · Ref %2$@" : { + "comment" : "Format string for pod session log row summary with a ref string: (1: fault code) (2: pdm ref string)" + }, "Fault event occurred" : { "comment" : "Pod state when fault event has occurred", "extractionState" : "manual", @@ -26290,6 +26311,9 @@ } } }, + "Forever" : { + "comment" : "Title string for PodSessionLogRetention.forever" + }, "Generating App Attest key…" : { "comment" : "O5 fetch progress: generate key" }, @@ -30997,6 +31021,9 @@ } } }, + "Keep Log For" : { + "comment" : "Label for pod session log retention picker" + }, "Keep the RileyLink about 6 inches from the pod during pairing." : { "comment" : "Label text for step 2 of Eros pair pod instructions", "extractionState" : "manual", @@ -31645,6 +31672,9 @@ } } }, + "Log Settings" : { + "comment" : "Section header for pod session log settings" + }, "Load custom certificate" : { "comment" : "Menu action to import an o5keypair file" }, @@ -36508,6 +36538,9 @@ } } }, + "No fault" : { + "comment" : "Pod session log row summary when the pod had no fault" + }, "No faults" : { "comment" : "Description for Fault Event Code .noFaults", "extractionState" : "manual", @@ -37156,6 +37189,9 @@ } } }, + "No pod sessions recorded yet." : { + "comment" : "Text shown when the pod session log is empty" + }, "No pods found" : { "comment" : "Error message for PodCommsError.noPodsFound", "extractionState" : "manual", @@ -50616,6 +50652,12 @@ } } }, + "Pod Session Log" : { + "comment" : "Navigation title for the pod session log screen\nText for Pod Session Log row and page" + }, + "Pod Sessions" : { + "comment" : "Section header for pod session log entries" + }, "Pod Setup" : { "comment" : "Title for PodSetupView", "extractionState" : "manual", @@ -72052,6 +72094,9 @@ } } }, + "Unknown Date" : { + "comment" : "Pod session log row date when no date is available" + }, "Unknown Omnipod Pod Type" : { "comment" : "Description for an unknown Omnipod pod type", "extractionState" : "manual", diff --git a/OmnipodKit/OmnipodCommon/PodSessionLogRetention.swift b/OmnipodKit/OmnipodCommon/PodSessionLogRetention.swift new file mode 100644 index 0000000..aa663d4 --- /dev/null +++ b/OmnipodKit/OmnipodCommon/PodSessionLogRetention.swift @@ -0,0 +1,55 @@ +// +// PodSessionLogRetention.swift +// OmnipodKit +// +// Created for the pod session log feature. +// Copyright © 2026 LoopKit Authors. All rights reserved. +// + +import Foundation + +/// How long completed pod sessions are kept in the pod session log before +/// being pruned. Each pod session (normal deactivation, expiration, or fault) +/// is recorded with its activation/end dates and full pod status, including +/// fault code and PDM ref string when applicable, so it can be reviewed later +/// or reported to the pod manufacturer without relying on screenshots. +enum PodSessionLogRetention: Int, CaseIterable, Codable { + case days30 + case days90 + case days180 + case days365 + case forever + + static let `default`: PodSessionLogRetention = .days90 + + /// Number of days to retain entries, or nil to keep the log forever. + var days: Int? { + switch self { + case .days30: + return 30 + case .days90: + return 90 + case .days180: + return 180 + case .days365: + return 365 + case .forever: + return nil + } + } + + var title: String { + switch self { + case .days30: + return LocalizedString("30 Days", comment: "Title string for PodSessionLogRetention.days30") + case .days90: + return LocalizedString("90 Days", comment: "Title string for PodSessionLogRetention.days90") + case .days180: + return LocalizedString("180 Days", comment: "Title string for PodSessionLogRetention.days180") + case .days365: + return LocalizedString("1 Year", comment: "Title string for PodSessionLogRetention.days365") + case .forever: + return LocalizedString("Forever", comment: "Title string for PodSessionLogRetention.forever") + } + } +} diff --git a/OmnipodKit/PumpManager/OmniPumpManager.swift b/OmnipodKit/PumpManager/OmniPumpManager.swift index 42d846f..833b64a 100644 --- a/OmnipodKit/PumpManager/OmniPumpManager.swift +++ b/OmnipodKit/PumpManager/OmniPumpManager.swift @@ -778,6 +778,39 @@ extension OmniPumpManager { } } + // Completed pod sessions currently retained in the session log, most recent first. + var podSessionLog: [PodState] { + state.podSessionLog + } + + // Deletes specific pod session log entries (e.g. via swipe-to-delete in the UI). + func deletePodSessionLogEntries(at offsets: IndexSet) { + setState { state in + state.removeSessionLogEntries(at: offsets) + } + } + + // Clears the entire pod session log. + func clearPodSessionLog() { + setState { state in + state.clearSessionLog() + } + } + + var podSessionLogRetention: PodSessionLogRetention { + set { + setState { (state) in + state.podSessionLogRetention = newValue + // Apply the new retention window immediately rather than waiting + // for the next pod session to end. + state.pruneSessionLog() + } + } + get { + state.podSessionLogRetention + } + } + var defaultLowReservoirReminderValue: Double { set { setState { (state) in @@ -1035,6 +1068,9 @@ extension OmniPumpManager { if podState.deliveryStoppedAt == nil { state.previousPodState?.deliveryStoppedAt = Date() } + if let completedPodState = state.previousPodState { + state.recordCompletedPodSession(completedPodState) + } } switch podType { diff --git a/OmnipodKit/PumpManager/OmniPumpManagerState.swift b/OmnipodKit/PumpManager/OmniPumpManagerState.swift index 56be2b2..18b4533 100644 --- a/OmnipodKit/PumpManager/OmniPumpManagerState.swift +++ b/OmnipodKit/PumpManager/OmniPumpManagerState.swift @@ -63,6 +63,14 @@ public struct OmniPumpManagerState: RawRepresentable, Equatable { // for user review and manufacturer reporting. internal var previousPodState: PodState? + // Rolling log of completed pod sessions (deactivated, expired, or faulted), + // most recent first, for user review and manufacturer reporting without + // needing to rely on screenshots. Pruned according to podSessionLogRetention. + internal var podSessionLog: [PodState] = [] + + // How long completed pod sessions are kept in podSessionLog before being pruned. + var podSessionLogRetention: PodSessionLogRetention = .default + // Indicates that the user has completed initial configuration // which means they have configured any parameters, but may not have paired a pod yet. var initialConfigurationCompleted: Bool = false @@ -347,6 +355,19 @@ public struct OmniPumpManagerState: RawRepresentable, Equatable { self.previousPodState = nil } + if let rawSessionLog = rawValue["podSessionLog"] as? [PodState.RawValue] { + self.podSessionLog = rawSessionLog.compactMap { PodState(rawValue: $0) } + } else { + self.podSessionLog = [] + } + + if let rawRetention = rawValue["podSessionLogRetention"] as? Int, + let retention = PodSessionLogRetention(rawValue: rawRetention) { + self.podSessionLogRetention = retention + } else { + self.podSessionLogRetention = .default + } + if podType.isEros { // Some more Eros specific values if let pairingAttemptAddress = rawValue["pairingAttemptAddress"] as? UInt32 { @@ -387,12 +408,44 @@ public struct OmniPumpManagerState: RawRepresentable, Equatable { value["lastPumpDataReportDate"] = lastPumpDataReportDate value["silencePodEnd"] = silencePodEnd value["previousPodState"] = previousPodState?.rawValue + value["podSessionLog"] = podSessionLog.map { $0.rawValue } + value["podSessionLogRetention"] = podSessionLogRetention.rawValue return value } } extension OmniPumpManagerState { + // Records a just-completed pod session (call once, when a pod session ends) + // and prunes the log per the current retention setting. + mutating func recordCompletedPodSession(_ podState: PodState) { + podSessionLog.insert(podState, at: 0) + pruneSessionLog() + } + + // Removes podSessionLog entries older than podSessionLogRetention allows. + // Safe to call any time, e.g. right after the user changes the retention setting. + mutating func pruneSessionLog() { + guard let retentionDays = podSessionLogRetention.days else { + return // .forever: keep everything + } + let cutoff = Date().addingTimeInterval(-.days(Double(retentionDays))) + podSessionLog.removeAll { session in + let sessionEndDate = session.deliveryStoppedAt ?? session.activatedAt ?? .distantPast + return sessionEndDate < cutoff + } + } + + // Removes specific podSessionLog entries, e.g. from a swipe-to-delete action. + mutating func removeSessionLogEntries(at offsets: IndexSet) { + podSessionLog.remove(atOffsets: offsets) + } + + // Empties the entire podSessionLog, e.g. from a "Clear Log" action. + mutating func clearSessionLog() { + podSessionLog.removeAll() + } + var hasActivePod: Bool { return podState?.isActive == true } @@ -437,6 +490,8 @@ extension OmniPumpManagerState: CustomDebugStringConvertible { "* acknowledgedTimeOffsetAlert: \(acknowledgedTimeOffsetAlert)", "* initialConfigurationCompleted: \(initialConfigurationCompleted)", "* podType: \(podType)", + "* podSessionLogRetention: \(podSessionLogRetention)", + "* podSessionLog: \(podSessionLog.count) session(s)", "", ].joined(separator: "\n") if podType.usesRileyLink { diff --git a/OmnipodKit/PumpManagerUI/ViewModels/OmniSettingsViewModel.swift b/OmnipodKit/PumpManagerUI/ViewModels/OmniSettingsViewModel.swift index 84aebdc..5648244 100644 --- a/OmnipodKit/PumpManagerUI/ViewModels/OmniSettingsViewModel.swift +++ b/OmnipodKit/PumpManagerUI/ViewModels/OmniSettingsViewModel.swift @@ -146,6 +146,17 @@ class OmniSettingsViewModel: ObservableObject { @Published var previousPodDetails: PodDetails? @Published var controllerId: UInt32 + + @Published var podSessionLogDetails: [PodDetails] = [] + + @Published var podSessionLogRetention: PodSessionLogRetention { + didSet { + self.pumpManager.podSessionLogRetention = podSessionLogRetention + // Changing retention may prune entries; refresh what's shown right away. + self.podSessionLogDetails = self.pumpManager.podSessionLogDetails + } + } + var timeZone: TimeZone { return pumpManager.status.timeZone @@ -276,6 +287,8 @@ class OmniSettingsViewModel: ObservableObject { podDetails = pumpManager.podDetails previousPodDetails = pumpManager.previousPodDetails controllerId = pumpManager.state.controllerId + podSessionLogDetails = pumpManager.podSessionLogDetails + podSessionLogRetention = pumpManager.podSessionLogRetention pumpManager.addPodStateObserver(self, queue: DispatchQueue.main) pumpManager.addStatusObserver(self, queue: DispatchQueue.main) @@ -432,6 +445,16 @@ class OmniSettingsViewModel: ObservableObject { self.podKeepAlivePreference = podKeepAlivePreference } + func deletePodSessionLogEntries(at offsets: IndexSet) { + pumpManager.deletePodSessionLogEntries(at: offsets) + self.podSessionLogDetails = pumpManager.podSessionLogDetails + } + + func clearPodSessionLog() { + pumpManager.clearPodSessionLog() + self.podSessionLogDetails = pumpManager.podSessionLogDetails + } + func didChangeInsulinType(_ newType: InsulinType?) { self.pumpManager.insulinType = newType } @@ -586,6 +609,7 @@ extension OmniSettingsViewModel: PodStateObserver { insulinType = self.pumpManager.insulinType podDetails = self.pumpManager.podDetails previousPodDetails = self.pumpManager.previousPodDetails + podSessionLogDetails = self.pumpManager.podSessionLogDetails } func podConnectionStateDidChange(isConnected: Bool) { @@ -693,6 +717,13 @@ extension OmniPumpManager { return podDetails(fromPodState: podState, andDeviceName: nil) } + // Completed pod sessions currently retained in the session log, most recent first, + // for display in PodSessionLogView. Tapping an entry reuses PodDetailsView, same as + // the single-slot "Previous Pod" screen. + var podSessionLogDetails: [PodDetails] { + return state.podSessionLog.map { podDetails(fromPodState: $0, andDeviceName: nil) } + } + } extension OmniPumpManager: DiagnosticCommands { diff --git a/OmnipodKit/PumpManagerUI/Views/OmniSettingsView.swift b/OmnipodKit/PumpManagerUI/Views/OmniSettingsView.swift index 2961922..fa02fcc 100644 --- a/OmnipodKit/PumpManagerUI/Views/OmniSettingsView.swift +++ b/OmnipodKit/PumpManagerUI/Views/OmniSettingsView.swift @@ -494,6 +494,17 @@ struct OmniSettingsView: View { .foregroundColor(Color.secondary) } } + + let localizedPodSessionLogStr = LocalizedString("Pod Session Log", comment: "Text for Pod Session Log row and page") + NavigationLink(destination: PodSessionLogView(viewModel: viewModel)) { + HStack { + Text(localizedPodSessionLogStr) + .foregroundColor(Color.primary) + Spacer() + Text(String(viewModel.podSessionLogDetails.count)) + .foregroundColor(Color.secondary) + } + } } Section(header: SectionHeader(label: LocalizedString("Configuration", comment: "Section header for configuration section"))) diff --git a/OmnipodKit/PumpManagerUI/Views/PodDetailsView.swift b/OmnipodKit/PumpManagerUI/Views/PodDetailsView.swift index cd92421..83a664f 100644 --- a/OmnipodKit/PumpManagerUI/Views/PodDetailsView.swift +++ b/OmnipodKit/PumpManagerUI/Views/PodDetailsView.swift @@ -133,6 +133,7 @@ struct PodDetailsView: View { } else { row(LocalizedString("Deactivation", comment: "description label for deactivation pod details row"), value: dateFormatter.string(from: deliveryStoppedAt)) } + } else { row(LocalizedString("Last Status", comment: "description label for last status date pod details row"), value: lastStatusText) } diff --git a/OmnipodKit/PumpManagerUI/Views/PodSessionLogView.swift b/OmnipodKit/PumpManagerUI/Views/PodSessionLogView.swift new file mode 100644 index 0000000..0d9bcd4 --- /dev/null +++ b/OmnipodKit/PumpManagerUI/Views/PodSessionLogView.swift @@ -0,0 +1,247 @@ +// +// PodSessionLogView.swift +// OmnipodKit +// +// Created for the pod session log feature. +// Copyright © 2026 LoopKit Authors. All rights reserved. +// + +import SwiftUI +import LoopKitUI + +struct PodSessionLogView: View { + @ObservedObject var viewModel: OmniSettingsViewModel + + var body: some View { + PodSessionLogListContent( + details: viewModel.podSessionLogDetails, + retention: $viewModel.podSessionLogRetention, + onDelete: viewModel.deletePodSessionLogEntries, + onClearAll: viewModel.clearPodSessionLog + ) + } +} + + +/// Pure-data content view, split out so it can be previewed without a live OmniPumpManager. +struct PodSessionLogListContent: View { + var details: [PodDetails] + @Binding var retention: PodSessionLogRetention + var onDelete: (IndexSet) -> Void = { _ in } + var onClearAll: () -> Void = {} + + @Environment(\.guidanceColors) var guidanceColors + + @State private var showingClearConfirmation = false + + let dateFormatter: DateFormatter = { + let dateFormatter = DateFormatter() + dateFormatter.timeStyle = .short + dateFormatter.dateStyle = .medium + dateFormatter.doesRelativeDateFormatting = true + return dateFormatter + }() + + private func rowDate(for details: PodDetails) -> String { + let date = details.deliveryStoppedAt ?? details.activatedAt + guard let date = date else { + return LocalizedString("Unknown Date", comment: "Pod session log row date when no date is available") + } + return dateFormatter.string(from: date) + } + + private func rowSummary(for details: PodDetails) -> String { + if let fault = details.fault { + let faultCode = fault.faultEventCode + if let pdmRef = fault.pdmRef { + return String(format: LocalizedString("Fault %1$@ · Ref %2$@", comment: "Format string for pod session log row summary with a ref string: (1: fault code) (2: pdm ref string)"), String(format: "%03u", faultCode.rawValue), pdmRef) + } else { + return String(format: LocalizedString("Fault %1$@ · %2$@", comment: "Format string for pod session log row summary: (1: fault code) (2: fault description)"), String(format: "%03u", faultCode.rawValue), faultCode.faultDescription) + } + } else { + return LocalizedString("No fault", comment: "Pod session log row summary when the pod had no fault") + } + } + + private func row(for details: PodDetails) -> some View { + NavigationLink(destination: PodDetailsView(podDetails: details, title: rowDate(for: details))) { + HStack { + if details.fault != nil { + Image(systemName: "exclamationmark.triangle.fill") + .foregroundColor(guidanceColors.critical) + } + VStack(alignment: .leading, spacing: 2) { + Text(rowDate(for: details)) + .foregroundColor(.primary) + Text(rowSummary(for: details)) + .font(.caption) + .foregroundColor(.secondary) + } + } + } + } + + var body: some View { + List { + Section(header: Text(LocalizedString("Log Settings", comment: "Section header for pod session log settings"))) { + Picker(LocalizedString("Keep Log For", comment: "Label for pod session log retention picker"), selection: $retention) { + ForEach(PodSessionLogRetention.allCases, id: \.self) { retention in + Text(retention.title).tag(retention) + } + } + .pickerStyle(MenuPickerStyle()) + if $retention.wrappedValue == .forever { + Button(role: .destructive, action: { + showingClearConfirmation = true + }) { + Text(LocalizedString("Clear Pod Session Log", comment: "Button title to clear the entire pod session log")) + } + .disabled(details.isEmpty) + // Attach the dialog here so it anchors to the button on iPad + .confirmationDialog( + LocalizedString("Clear Pod Session Log?", comment: "Title for clear pod session log action sheet"), + isPresented: $showingClearConfirmation, + titleVisibility: .visible + ) { + Button(role: .destructive) { + onClearAll() + } label: { + Text(LocalizedString("Clear Log", comment: "Button title to confirm clearing the pod session log")) + } + + Button(role: .cancel) { + // Cancel action (handled automatically, but defined here) + } label: { + Text(LocalizedString("Cancel", comment: "Cancel button label")) + } + } message: { + Text(LocalizedString("This will permanently delete all recorded pod sessions. This cannot be undone.", comment: "Message for clear pod session log action sheet")) + } + } + } + + Section( + header: Text(LocalizedString("Pod Sessions", comment: "Section header for pod session log entries")), + footer: Text(LocalizedString("Each completed pod is recorded here with its fault code and ref string.", comment: "Footer explanation for the pod session log")) + ) { + if details.isEmpty { + Text(LocalizedString("No pod sessions recorded yet.", comment: "Text shown when the pod session log is empty")) + .foregroundColor(.secondary) + } else { + ForEach(Array(details.enumerated()), id: \.offset) { _, details in + row(for: details) + } + .onDelete(perform: onDelete) + } + } + } + .navigationTitle(LocalizedString("Pod Session Log", comment: "Navigation title for the pod session log screen")) + .navigationBarTitleDisplayMode(.automatic) + } +} + +#if DEBUG +struct PodSessionLogListContent_Previews: PreviewProvider { + + /// Builds a real, decodable DetailedStatus fault by hand-constructing the + /// 22-byte encoded payload its `init(encodedData:)` expects. + /// (DetailedStatus has no memberwise initializer — it only parses raw pod bytes.) + static func mockFault(faultCode: UInt8) -> DetailedStatus { + var bytes = [UInt8](repeating: 0, count: 22) + bytes[1] = PodProgressStatus.faultEventOccurred.rawValue // podProgressStatus + bytes[2] = 0 // deliveryStatus (overridden to suspended since faulted) + bytes[3] = 0; bytes[4] = 0 // bolusNotDelivered + bytes[5] = 1 // lastProgrammingMessageSeqNum + bytes[6] = 0; bytes[7] = 50 // totalInsulinDelivered + bytes[8] = faultCode // faultEventCode + bytes[9] = 0; bytes[10] = 120 // faultEventTimeSinceActivation (minutes) + bytes[11] = 0; bytes[12] = 100 // reservoirLevel + bytes[13] = 0; bytes[14] = 200 // timeActive (minutes) + bytes[15] = 0 // unacknowledgedAlerts + bytes[16] = 0 // faultAccessingTables + bytes[17] = 0x02 // errorEventInfo (non-zero so previousPodProgressStatus is valid) + bytes[18] = 0 // WW: 0 selects Dash-style pdmRef + bytes[19] = 0 + bytes[20] = 0x12; bytes[21] = 0x34 // possibleFaultCallingAddress + + return try! DetailedStatus(encodedData: Data(bytes)) + } + + static var mockDetails: [PodDetails] { + let calendar = Calendar.current + let now = Date() + + func daysAgo(_ days: Double) -> Date { + calendar.date(byAdding: .minute, value: -Int(days * 24 * 60), to: now)! + } + + /// - Parameters: + /// - startedDaysAgo: when the pod was activated + /// - durationHours: how long it ran before deliveryStoppedAt (short = manually removed early, ~72h = ran full life) + func detail(startedDaysAgo: Double, durationHours: Double, fault: DetailedStatus?, totalDelivery: Double) -> PodDetails { + let activatedAt = daysAgo(startedDaysAgo) + let stoppedAt = calendar.date(byAdding: .minute, value: Int(durationHours * 60), to: activatedAt)! + return PodDetails( + podType: PodType(rawValue: 4), + address: 0x17012345, + lotNumber: 123456789, + sequenceNumber: 1234567, + firmwareVersion: "4.3.2", + bleFirmwareVersion: "1.2.3", + deviceName: "DashPreviewPod", + totalDelivery: totalDelivery, + lastStatus: stoppedAt, + fault: fault, + activatedAt: activatedAt, + deliveryStoppedAt: stoppedAt, + podTime: .hours(durationHours) + ) + } + + return [ + // Normal full-life completions + detail(startedDaysAgo: 1, durationHours: 72, fault: nil, totalDelivery: 61.4), + detail(startedDaysAgo: 4, durationHours: 71.5, fault: nil, totalDelivery: 58.2), + + // Manually ended early (no fault, short duration -> user pulled it early) + detail(startedDaysAgo: 2, durationHours: 4.2, fault: nil, totalDelivery: 3.1), + detail(startedDaysAgo: 9, durationHours: 0.5, fault: nil, totalDelivery: 0.2), // removed almost immediately + + // Occlusion fault + detail(startedDaysAgo: 3, durationHours: 18, fault: mockFault(faultCode: 0x14), totalDelivery: 14.6), + + // Reservoir empty fault + detail(startedDaysAgo: 5, durationHours: 68, fault: mockFault(faultCode: 0x18), totalDelivery: 49.9), + + // Insulin delivery command error + detail(startedDaysAgo: 6, durationHours: 30, fault: mockFault(faultCode: 0x31), totalDelivery: 22.3), + + // Illegal reset fault + detail(startedDaysAgo: 7, durationHours: 2, fault: mockFault(faultCode: 0x34), totalDelivery: 0.8), + + // Exceeded max pod life (80hr) fault + detail(startedDaysAgo: 8, durationHours: 80, fault: mockFault(faultCode: 0x1C), totalDelivery: 65.0), + + // Encoder count too high fault + detail(startedDaysAgo: 11, durationHours: 40, fault: mockFault(faultCode: 0x40), totalDelivery: 31.7), + ] + } + + struct PreviewWrapper: View { + @State var retention: PodSessionLogRetention = .days365 + + var body: some View { + NavigationView { + PodSessionLogListContent( + details: PodSessionLogListContent_Previews.mockDetails, + retention: $retention + ) + } + } + } + + static var previews: some View { + PreviewWrapper() + } +} +#endif