aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--DomainDig/DomainViewModel+Export.swift180
-rw-r--r--DomainDig/DomainViewModel.swift175
2 files changed, 184 insertions, 171 deletions
diff --git a/DomainDig/DomainViewModel+Export.swift b/DomainDig/DomainViewModel+Export.swift
new file mode 100644
index 0000000..5ca0514
--- /dev/null
+++ b/DomainDig/DomainViewModel+Export.swift
@@ -0,0 +1,180 @@
+import Foundation
+
+/// Export and data-portability surface of `DomainViewModel`: rendering the
+/// current report, batch results, tracked domains, timelines, and workflow runs
+/// into the shareable formats, plus the full-backup / portable-slice exporters
+/// and the import + app-settings persistence entry points.
+///
+/// These are thin adapters over `DomainReportExporter` and
+/// `DomainDataPortabilityService`. The report-projection helpers they call
+/// (`currentBatchReports`, `reports(for:)`, `timelineReports`, `workflowReports`)
+/// remain on the main type — they belong to the report layer shared with
+/// inspection, not to export.
+extension DomainViewModel {
+ func exportJSONData() -> Data? {
+ guard let currentReport else { return nil }
+ return try? DomainReportExporter.data(for: currentReport, format: .json)
+ }
+
+ func exportJSONString() -> String? {
+ guard let data = exportJSONData() else { return nil }
+ return String(data: data, encoding: .utf8)
+ }
+
+ func exportSingleReportData(format: DomainExportFormat) -> Data? {
+ guard let currentReport else { return nil }
+ return try? DomainReportExporter.data(for: currentReport, format: format)
+ }
+
+ func exportBatchReportData(format: DomainExportFormat) -> Data? {
+ try? DomainReportExporter.data(
+ for: currentBatchReports(),
+ format: format,
+ title: batchLookupSource == .watchlistRefresh ? "Tracked Domains Export" : "Batch Results Export"
+ )
+ }
+
+ func exportTrackedDomainsData(domains: [TrackedDomain], format: DomainExportFormat) -> Data? {
+ try? DomainReportExporter.data(
+ for: reports(for: domains),
+ format: format,
+ title: "Tracked Domains Export"
+ )
+ }
+
+ func exportTimelineText(domain: String, includeDiffSummary: Bool) -> String {
+ DomainReportExporter.timelineText(
+ for: timelineReports(for: domain),
+ domain: domain,
+ includeDiffSummary: includeDiffSummary
+ )
+ }
+
+ func exportTimelineJSONData(domain: String, includeDiffSummary: Bool) -> Data? {
+ try? DomainReportExporter.timelineData(
+ for: timelineReports(for: domain),
+ domain: domain,
+ includeDiffSummary: includeDiffSummary
+ )
+ }
+
+ func exportFullBackupData() -> Data? {
+ try? DomainDataPortabilityService.backupData()
+ }
+
+ func exportPortableTrackedDomainsJSONData() -> Data? {
+ try? DomainDataPortabilityService.trackedDomainsExportData()
+ }
+
+ func exportPortableTrackedDomainsCSV() -> String {
+ DomainDataPortabilityService.trackedDomainsCSV()
+ }
+
+ func exportPortableWorkflowsJSONData() -> Data? {
+ try? DomainDataPortabilityService.workflowsExportData()
+ }
+
+ func exportPortableWorkflowsCSV() -> String {
+ DomainDataPortabilityService.workflowsCSV()
+ }
+
+ func exportPortableHistoryJSONData() -> Data? {
+ try? DomainDataPortabilityService.historyExportData()
+ }
+
+ func prepareDataImport(
+ data: Data,
+ fileName: String,
+ mode: DataPortabilityImportMode
+ ) throws -> DataImportPreview {
+ try DomainDataPortabilityService.prepareImport(data: data, fileName: fileName, mode: mode)
+ }
+
+ func applyDataImport(_ preview: DataImportPreview, mode: DataPortabilityImportMode) throws -> DataImportResult {
+ let result = try DomainDataPortabilityService.applyImport(preview, mode: mode)
+ refreshPersistedData()
+ portabilityStatusMessage = result.summary
+ CloudSyncService.shared.markAppSettingsChanged()
+ CloudSyncService.shared.markMonitoringSettingsChanged(localActivationConfirmed: monitoringSettings.isEnabled)
+ CloudSyncService.shared.scheduleSyncIfNeeded(trigger: .imported)
+ return result
+ }
+
+ func persistCurrentAppSettings(resolverURLString: String, appDensityRawValue: String) {
+ DomainDataPortabilityService.saveAppSettings(
+ AppSettingsSnapshot(
+ recentSearches: recentSearches,
+ savedDomains: savedDomains,
+ resolverURLString: resolverURLString,
+ appDensityRawValue: appDensityRawValue
+ )
+ )
+ CloudSyncService.shared.markAppSettingsChanged()
+ refreshDataLifecycleSummary()
+ }
+
+ func exportWorkflowText(summary: WorkflowRunSummary, changedOnly: Bool) -> String {
+ let reports = workflowReports(from: summary, changedOnly: changedOnly)
+ let base = DomainReportExporter.batchText(
+ for: reports,
+ title: "\(summary.workflowName) Workflow Export"
+ )
+ guard !summary.workflowInsights.isEmpty else { return base }
+ let insightLines = summary.workflowInsights.map {
+ "- \($0.description): \($0.domainsInvolved.joined(separator: ", "))"
+ }
+ return ([ "\(summary.workflowName) Workflow Insights", String(repeating: "-", count: 32) ] + insightLines + ["", base]).joined(separator: "\n")
+ }
+
+ func exportWorkflowCSV(summary: WorkflowRunSummary, changedOnly: Bool) -> String {
+ DomainReportExporter.csv(
+ for: workflowReports(from: summary, changedOnly: changedOnly),
+ workflowInsights: summary.workflowInsights
+ )
+ }
+
+ func exportWorkflowJSONData(summary: WorkflowRunSummary, changedOnly: Bool) -> Data? {
+ let payload = WorkflowExportPayload(
+ workflowName: summary.workflowName,
+ generatedAt: summary.generatedAt,
+ workflowInsights: summary.workflowInsights,
+ reports: workflowReports(from: summary, changedOnly: changedOnly)
+ )
+ let encoder = JSONEncoder()
+ encoder.outputFormatting = [.prettyPrinted, .sortedKeys]
+ encoder.dateEncodingStrategy = .iso8601
+ return try? encoder.encode(payload)
+ }
+
+ func exportWorkflowMarkdown(summary: WorkflowRunSummary, changedOnly: Bool) -> String {
+ let reports = workflowReports(from: summary, changedOnly: changedOnly)
+ let base = DomainReportExporter.batchMarkdown(for: reports, title: "\(summary.workflowName) Workflow Export")
+ guard !summary.workflowInsights.isEmpty else { return base }
+ let insightLines = summary.workflowInsights.map {
+ "- \($0.description): \($0.domainsInvolved.joined(separator: ", "))"
+ }
+ return (["## \(summary.workflowName) Workflow Insights"] + insightLines + ["", base]).joined(separator: "\n")
+ }
+
+ func exportWorkflowData(summary: WorkflowRunSummary, changedOnly: Bool, format: DomainExportFormat) -> Data? {
+ switch format {
+ case .text:
+ return Data(exportWorkflowText(summary: summary, changedOnly: changedOnly).utf8)
+ case .csv:
+ return Data(exportWorkflowCSV(summary: summary, changedOnly: changedOnly).utf8)
+ case .json:
+ return exportWorkflowJSONData(summary: summary, changedOnly: changedOnly)
+ case .markdown:
+ return Data(exportWorkflowMarkdown(summary: summary, changedOnly: changedOnly).utf8)
+ case .pdf:
+ return DomainReportExporter.pdfData(fromMarkdown: exportWorkflowMarkdown(summary: summary, changedOnly: changedOnly))
+ }
+ }
+}
+
+private struct WorkflowExportPayload: Codable {
+ let workflowName: String
+ let generatedAt: Date
+ let workflowInsights: [WorkflowInsight]
+ let reports: [DomainReport]
+}
diff --git a/DomainDig/DomainViewModel.swift b/DomainDig/DomainViewModel.swift
index 3814704..250d46d 100644
--- a/DomainDig/DomainViewModel.swift
+++ b/DomainDig/DomainViewModel.swift
@@ -110,13 +110,6 @@ private struct BatchLookupPayload {
let snapshot: LookupSnapshot
}
-private struct WorkflowExportPayload: Codable {
- let workflowName: String
- let generatedAt: Date
- let workflowInsights: [WorkflowInsight]
- let reports: [DomainReport]
-}
-
struct PortfolioDomainStatus: Identifiable {
let trackedDomain: TrackedDomain
let latestEntry: HistoryEntry?
@@ -1370,21 +1363,6 @@ final class DomainViewModel {
}
}
- func exportJSONData() -> Data? {
- guard let currentReport else { return nil }
- return try? DomainReportExporter.data(for: currentReport, format: .json)
- }
-
- func exportJSONString() -> String? {
- guard let data = exportJSONData() else { return nil }
- return String(data: data, encoding: .utf8)
- }
-
- func exportSingleReportData(format: DomainExportFormat) -> Data? {
- guard let currentReport else { return nil }
- return try? DomainReportExporter.data(for: currentReport, format: format)
- }
-
@discardableResult
func startAudit(for domain: String, reviewer: String? = nil) async -> AuditSession? {
let normalizedDomain = domain
@@ -1533,151 +1511,6 @@ final class DomainViewModel {
usageCredits = Dictionary(uniqueKeysWithValues: statuses.map { ($0.feature, $0) })
}
- func exportBatchReportData(format: DomainExportFormat) -> Data? {
- try? DomainReportExporter.data(
- for: currentBatchReports(),
- format: format,
- title: batchLookupSource == .watchlistRefresh ? "Tracked Domains Export" : "Batch Results Export"
- )
- }
-
- func exportTrackedDomainsData(domains: [TrackedDomain], format: DomainExportFormat) -> Data? {
- try? DomainReportExporter.data(
- for: reports(for: domains),
- format: format,
- title: "Tracked Domains Export"
- )
- }
-
- func exportTimelineText(domain: String, includeDiffSummary: Bool) -> String {
- DomainReportExporter.timelineText(
- for: timelineReports(for: domain),
- domain: domain,
- includeDiffSummary: includeDiffSummary
- )
- }
-
- func exportTimelineJSONData(domain: String, includeDiffSummary: Bool) -> Data? {
- try? DomainReportExporter.timelineData(
- for: timelineReports(for: domain),
- domain: domain,
- includeDiffSummary: includeDiffSummary
- )
- }
-
- func exportFullBackupData() -> Data? {
- try? DomainDataPortabilityService.backupData()
- }
-
- func exportPortableTrackedDomainsJSONData() -> Data? {
- try? DomainDataPortabilityService.trackedDomainsExportData()
- }
-
- func exportPortableTrackedDomainsCSV() -> String {
- DomainDataPortabilityService.trackedDomainsCSV()
- }
-
- func exportPortableWorkflowsJSONData() -> Data? {
- try? DomainDataPortabilityService.workflowsExportData()
- }
-
- func exportPortableWorkflowsCSV() -> String {
- DomainDataPortabilityService.workflowsCSV()
- }
-
- func exportPortableHistoryJSONData() -> Data? {
- try? DomainDataPortabilityService.historyExportData()
- }
-
- func prepareDataImport(
- data: Data,
- fileName: String,
- mode: DataPortabilityImportMode
- ) throws -> DataImportPreview {
- try DomainDataPortabilityService.prepareImport(data: data, fileName: fileName, mode: mode)
- }
-
- func applyDataImport(_ preview: DataImportPreview, mode: DataPortabilityImportMode) throws -> DataImportResult {
- let result = try DomainDataPortabilityService.applyImport(preview, mode: mode)
- refreshPersistedData()
- portabilityStatusMessage = result.summary
- CloudSyncService.shared.markAppSettingsChanged()
- CloudSyncService.shared.markMonitoringSettingsChanged(localActivationConfirmed: monitoringSettings.isEnabled)
- CloudSyncService.shared.scheduleSyncIfNeeded(trigger: .imported)
- return result
- }
-
- func persistCurrentAppSettings(resolverURLString: String, appDensityRawValue: String) {
- DomainDataPortabilityService.saveAppSettings(
- AppSettingsSnapshot(
- recentSearches: recentSearches,
- savedDomains: savedDomains,
- resolverURLString: resolverURLString,
- appDensityRawValue: appDensityRawValue
- )
- )
- CloudSyncService.shared.markAppSettingsChanged()
- refreshDataLifecycleSummary()
- }
-
- func exportWorkflowText(summary: WorkflowRunSummary, changedOnly: Bool) -> String {
- let reports = workflowReports(from: summary, changedOnly: changedOnly)
- let base = DomainReportExporter.batchText(
- for: reports,
- title: "\(summary.workflowName) Workflow Export"
- )
- guard !summary.workflowInsights.isEmpty else { return base }
- let insightLines = summary.workflowInsights.map {
- "- \($0.description): \($0.domainsInvolved.joined(separator: ", "))"
- }
- return ([ "\(summary.workflowName) Workflow Insights", String(repeating: "-", count: 32) ] + insightLines + ["", base]).joined(separator: "\n")
- }
-
- func exportWorkflowCSV(summary: WorkflowRunSummary, changedOnly: Bool) -> String {
- DomainReportExporter.csv(
- for: workflowReports(from: summary, changedOnly: changedOnly),
- workflowInsights: summary.workflowInsights
- )
- }
-
- func exportWorkflowJSONData(summary: WorkflowRunSummary, changedOnly: Bool) -> Data? {
- let payload = WorkflowExportPayload(
- workflowName: summary.workflowName,
- generatedAt: summary.generatedAt,
- workflowInsights: summary.workflowInsights,
- reports: workflowReports(from: summary, changedOnly: changedOnly)
- )
- let encoder = JSONEncoder()
- encoder.outputFormatting = [.prettyPrinted, .sortedKeys]
- encoder.dateEncodingStrategy = .iso8601
- return try? encoder.encode(payload)
- }
-
- func exportWorkflowMarkdown(summary: WorkflowRunSummary, changedOnly: Bool) -> String {
- let reports = workflowReports(from: summary, changedOnly: changedOnly)
- let base = DomainReportExporter.batchMarkdown(for: reports, title: "\(summary.workflowName) Workflow Export")
- guard !summary.workflowInsights.isEmpty else { return base }
- let insightLines = summary.workflowInsights.map {
- "- \($0.description): \($0.domainsInvolved.joined(separator: ", "))"
- }
- return (["## \(summary.workflowName) Workflow Insights"] + insightLines + ["", base]).joined(separator: "\n")
- }
-
- func exportWorkflowData(summary: WorkflowRunSummary, changedOnly: Bool, format: DomainExportFormat) -> Data? {
- switch format {
- case .text:
- return Data(exportWorkflowText(summary: summary, changedOnly: changedOnly).utf8)
- case .csv:
- return Data(exportWorkflowCSV(summary: summary, changedOnly: changedOnly).utf8)
- case .json:
- return exportWorkflowJSONData(summary: summary, changedOnly: changedOnly)
- case .markdown:
- return Data(exportWorkflowMarkdown(summary: summary, changedOnly: changedOnly).utf8)
- case .pdf:
- return DomainReportExporter.pdfData(fromMarkdown: exportWorkflowMarkdown(summary: summary, changedOnly: changedOnly))
- }
- }
-
private func performLookup(domain: String, lookupID: UUID) async -> HistoryEntry? {
let lookupStartedAt = DomainDebugLog.signpostStart("DomainViewModel.performLookup", domain: domain)
let previous = previousSnapshot(
@@ -3488,13 +3321,13 @@ final class DomainViewModel {
}
}
- private func currentBatchReports() -> [DomainReport] {
+ func currentBatchReports() -> [DomainReport] {
currentBatchResultEntries.map { entry in
report(for: entry, workflowContext: activeWorkflowContext)
}
}
- private func workflowReports(from summary: WorkflowRunSummary, changedOnly: Bool) -> [DomainReport] {
+ func workflowReports(from summary: WorkflowRunSummary, changedOnly: Bool) -> [DomainReport] {
let filteredResults = changedOnly ? summary.results.filter(\.hasMeaningfulChange) : summary.results
return filteredResults.compactMap { result in
guard let entry = historyEntry(for: result) else { return nil }
@@ -3509,7 +3342,7 @@ final class DomainViewModel {
}
}
- private func reports(for domains: [TrackedDomain]) -> [DomainReport] {
+ func reports(for domains: [TrackedDomain]) -> [DomainReport] {
let latestEntries = latestSnapshots(for: domains)
return domains.map { trackedDomain in
@@ -3524,7 +3357,7 @@ final class DomainViewModel {
}
}
- private func timelineReports(for domain: String) -> [DomainReport] {
+ func timelineReports(for domain: String) -> [DomainReport] {
historyEntries(for: domain).map { report(for: $0) }
}