diff options
| author | Christian Cleberg <[email protected]> | 2026-07-17 11:18:25 -0500 |
|---|---|---|
| committer | Christian Cleberg <[email protected]> | 2026-07-20 13:56:41 -0500 |
| commit | fd8ca178a379da71f5bb19c461524888cad9447e (patch) | |
| tree | adb481e4c7c22e41e919b97ccded0e506083f9b3 /DomainDig/ScheduledReportsView.swift | |
| parent | 2b52ee5520cad3c47d30d513946645366f268762 (diff) | |
| download | domain-dig-fd8ca178a379da71f5bb19c461524888cad9447e.tar.gz domain-dig-fd8ca178a379da71f5bb19c461524888cad9447e.tar.bz2 domain-dig-fd8ca178a379da71f5bb19c461524888cad9447e.zip | |
v4.8.0: Add scheduled report generation
- ScheduledReportService (@MainActor, headless/storage-backed like
DomainMonitoringService): builds the latest report for every tracked domain
from persisted history, exports it via DomainReportExporter in the
configured format (markdown/PDF/JSON), writes it to a local Documents
subdirectory, logs the run, and fires a "Scheduled Report Ready" local
notification.
- ScheduledReportScheduler mirrors DomainMonitoringScheduler's BGTaskScheduler
approach with its own task identifier (net.cleberg.DomainDig.report.schedule,
added to Info.plist) and a daily/weekly cadence.
- ScheduledReportsView (Settings → Scheduled Reports): enable toggle, cadence
and format pickers, "Generate Now", and a log of past reports each shareable
via the existing ExportPresenter share sheet.
- Gated behind the existing .automatedMonitoring capability (Pro), consistent
with monitoring being the other background-automation feature.
- Settings/logs persist via UserDefaults (DomainExportFormat is now Codable),
not DomainDataPortabilityService backup/restore — this is local automation
configuration, not user-authored content, same reasoning as v4.7.0's
watchlist saved views.
Diffstat (limited to 'DomainDig/ScheduledReportsView.swift')
| -rw-r--r-- | DomainDig/ScheduledReportsView.swift | 130 |
1 files changed, 130 insertions, 0 deletions
diff --git a/DomainDig/ScheduledReportsView.swift b/DomainDig/ScheduledReportsView.swift new file mode 100644 index 0000000..f97ee77 --- /dev/null +++ b/DomainDig/ScheduledReportsView.swift @@ -0,0 +1,130 @@ +import SwiftUI + +struct ScheduledReportsView: View { + @Environment(\.appDensity) private var appDensity + @State private var settings = ScheduledReportStorage.loadSettings() + @State private var logs = ScheduledReportStorage.loadLogs() + @State private var isGenerating = false + @State private var statusMessage: String? + + var body: some View { + List { + Section("Overview") { + VStack(alignment: .leading, spacing: 8) { + if !FeatureAccessService.hasAccess(to: .automatedMonitoring) { + Text("Scheduled reports require Pro.") + .font(appDensity.font(.caption)) + .foregroundStyle(.secondary) + } + + Toggle("Scheduled Reports", isOn: Binding( + get: { settings.isEnabled }, + set: { newValue in + settings.isEnabled = newValue + saveSettings() + } + )) + .disabled(!FeatureAccessService.hasAccess(to: .automatedMonitoring)) + + Picker("Cadence", selection: Binding( + get: { settings.cadence }, + set: { newValue in + settings.cadence = newValue + saveSettings() + } + )) { + ForEach(ScheduledReportCadence.allCases) { cadence in + Text(cadence.title).tag(cadence) + } + } + + Picker("Format", selection: Binding( + get: { settings.format }, + set: { newValue in + settings.format = newValue + saveSettings() + } + )) { + ForEach([DomainExportFormat.markdown, .pdf, .json]) { format in + Text(format.title).tag(format) + } + } + + LabeledContent( + "Last Generated", + value: settings.lastGeneratedAt?.formatted(date: .abbreviated, time: .shortened) ?? "Never" + ) + + if let statusMessage { + Text(statusMessage) + .font(appDensity.font(.caption)) + .foregroundStyle(.secondary) + } + + Button(isGenerating ? "Generating…" : "Generate Now") { + Task { await generateNow() } + } + .disabled(isGenerating) + } + .padding(.vertical, 4) + } + .listRowBackground(Color(.systemGray6).opacity(0.5)) + + if logs.isEmpty { + Section { + EmptyStateCardView( + title: "No Reports Yet", + message: "Generated reports for your watchlist appear here after a manual or scheduled run.", + suggestion: "Tap Generate Now, or enable Scheduled Reports above.", + systemImage: "doc.text", + showsCardBackground: false + ) + } + .listRowBackground(Color(.systemGray6).opacity(0.5)) + } else { + Section("Recent Reports") { + ForEach(logs) { log in + Button { + share(log) + } label: { + VStack(alignment: .leading, spacing: 2) { + Text("\(log.domainCount) domains • \(log.format.title)") + .foregroundStyle(.primary) + Text(log.generatedAt.formatted(date: .abbreviated, time: .shortened)) + .font(.caption) + .foregroundStyle(.secondary) + } + } + } + } + .listRowBackground(Color(.systemGray6).opacity(0.5)) + } + } + .scrollContentBackground(.hidden) + .background(Color.black) + .navigationTitle("Scheduled Reports") + } + + private func saveSettings() { + ScheduledReportStorage.saveSettings(settings) + ScheduledReportScheduler.shared.syncSchedule() + } + + private func generateNow() async { + isGenerating = true + let outcome = await ScheduledReportService.shared.generateReport(trigger: .manual, requireEnabledSetting: false) + statusMessage = outcome.message + settings = ScheduledReportStorage.loadSettings() + logs = ScheduledReportStorage.loadLogs() + isGenerating = false + } + + private func share(_ log: ScheduledReportLog) { + let url = ScheduledReportStorage.reportsDirectory.appendingPathComponent(log.fileName) + guard let data = try? Data(contentsOf: url) else { + statusMessage = "That report is no longer available on disk." + return + } + ExportPresenter.share(filename: log.fileName, data: data) + } +} |
