diff options
Diffstat (limited to 'DomainDig')
| -rw-r--r-- | DomainDig/ContentView.swift | 121 | ||||
| -rw-r--r-- | DomainDig/DataResetService.swift | 3 | ||||
| -rw-r--r-- | DomainDig/DomainDigApp.swift | 4 |
3 files changed, 127 insertions, 1 deletions
diff --git a/DomainDig/ContentView.swift b/DomainDig/ContentView.swift index c5c8e50..3716f88 100644 --- a/DomainDig/ContentView.swift +++ b/DomainDig/ContentView.swift @@ -2560,6 +2560,10 @@ struct SettingsView: View { IntegrationsSettingsView() } + NavigationLink("Local API") { + LocalAPISettingsView() + } + NavigationLink("iCloud Sync") { CloudSyncSettingsView() } @@ -2749,6 +2753,123 @@ private struct CloudSyncSettingsView: View { } } +private struct LocalAPISettingsView: View { + @Environment(\.appDensity) private var appDensity + @State private var localAPIService = LocalAPIService.shared + @State private var portText = "" + + var body: some View { + Form { + Section("Local API") { + Toggle( + "Enable Local API", + isOn: Binding( + get: { localAPIService.config.isEnabled }, + set: { localAPIService.setEnabled($0) } + ) + ) + + TextField( + "Port", + text: Binding( + get: { portText }, + set: { newValue in + portText = newValue + if let port = Int(newValue) { + localAPIService.setPort(port) + } + } + ) + ) + .keyboardType(.numberPad) + + LabeledContent("Address", value: localAPIService.address) + LabeledContent("Status", value: localAPIService.isRunning ? "Running" : (localAPIService.config.isEnabled ? "Stopped" : "Disabled")) + LabeledContent("Token", value: localAPIService.maskedToken) + + if let statusMessage = localAPIService.statusMessage { + Text(statusMessage) + .font(appDensity.font(.caption, design: .default)) + .foregroundStyle(.secondary) + } + } + + Section("Authentication") { + Button("Copy Token") { + localAPIService.copyToken() + } + + Button("Rotate Token") { + localAPIService.rotateToken() + } + + Text("Every request requires either `Authorization: Bearer <token>` or `X-API-Token`. DomainDig stores the token in Keychain and only binds the server to localhost.") + .font(appDensity.font(.caption, design: .default)) + .foregroundStyle(.secondary) + } + + Section("Request Logging") { + Toggle( + "Log Requests", + isOn: Binding( + get: { localAPIService.config.requestLoggingEnabled }, + set: { localAPIService.setRequestLoggingEnabled($0) } + ) + ) + + if localAPIService.requestLogs.isEmpty { + Text("No local API requests logged yet.") + .font(appDensity.font(.caption, design: .default)) + .foregroundStyle(.secondary) + } else { + ForEach(localAPIService.requestLogs.prefix(25)) { log in + VStack(alignment: .leading, spacing: 4) { + HStack { + Text("\(log.method) \(log.path)") + .font(appDensity.font(.callout, design: .monospaced)) + Spacer() + Text("\(log.statusCode)") + .font(appDensity.font(.caption, design: .default)) + .foregroundStyle(log.statusCode >= 400 ? .red : .secondary) + } + + Text(log.timestamp.formatted(date: .abbreviated, time: .standard)) + .font(appDensity.font(.caption2, design: .default)) + .foregroundStyle(.secondary) + + Text("\(Int(log.duration * 1000)) ms") + .font(appDensity.font(.caption2, design: .default)) + .foregroundStyle(.secondary) + } + } + } + + Button("Clear Logs", role: .destructive) { + localAPIService.clearRequestLogs() + } + } + + Section("Control") { + Button("Restart Server") { + localAPIService.setEnabled(false) + localAPIService.setEnabled(true) + } + .disabled(!localAPIService.config.isEnabled) + + Button("Stop Server") { + localAPIService.stopServer() + } + .disabled(!localAPIService.isRunning) + } + } + .navigationTitle("Local API") + .onAppear { + portText = String(localAPIService.config.port) + localAPIService.refresh() + } + } +} + private struct MonitoringSettingsView: View { @Environment(\.appDensity) private var appDensity @Bindable var viewModel: DomainViewModel diff --git a/DomainDig/DataResetService.swift b/DomainDig/DataResetService.swift index e993eba..d4c5bfe 100644 --- a/DomainDig/DataResetService.swift +++ b/DomainDig/DataResetService.swift @@ -15,7 +15,7 @@ enum DataResetService { static func wipeAllLocalData(viewModel: DomainViewModel) async throws { let secretReferences = await MainActor.run { - IntegrationService.shared.localSecretReferences() + IntegrationService.shared.localSecretReferences() + LocalAPIService.shared.localSecretReferences() } try await Task.detached(priority: .userInitiated) { @@ -28,6 +28,7 @@ enum DataResetService { await MainActor.run { IntegrationService.shared.resetAfterLocalWipe() + LocalAPIService.shared.resetAfterLocalWipe() CloudSyncService.shared.resetLocalStateAfterWipe() PurchaseService.shared.resetCachedStateAfterLocalWipe() _ = DomainMonitoringScheduler.shared.syncSchedule() diff --git a/DomainDig/DomainDigApp.swift b/DomainDig/DomainDigApp.swift index 2071213..ff40caf 100644 --- a/DomainDig/DomainDigApp.swift +++ b/DomainDig/DomainDigApp.swift @@ -15,6 +15,7 @@ struct DomainDigApp: App { @State private var viewModel = DomainViewModel() @State private var purchaseService = PurchaseService.shared @State private var cloudSyncService = CloudSyncService.shared + @State private var localAPIService = LocalAPIService.shared init() { LocalNotificationService.shared.configureForegroundPresentation() @@ -28,11 +29,13 @@ struct DomainDigApp: App { .task { let _ = purchaseService.currentTier let _ = cloudSyncService.status + let _ = localAPIService.isRunning let _ = IntegrationService.shared.targets.count await purchaseService.refreshEntitlements() viewModel.refreshMonitoringState() await viewModel.refreshMonitoringAuthorizationStatus() await cloudSyncService.refreshAvailability() + localAPIService.refresh() cloudSyncService.scheduleSyncIfNeeded(trigger: .launch) viewModel.monitoringStatusMessage = DomainMonitoringScheduler.shared.syncSchedule() IntegrationService.shared.processQueueNow() @@ -50,6 +53,7 @@ struct DomainDigApp: App { await viewModel.refreshMonitoringAuthorizationStatus() await cloudSyncService.refreshAvailability() } + localAPIService.refresh() cloudSyncService.scheduleSyncIfNeeded(trigger: .launch) viewModel.monitoringStatusMessage = DomainMonitoringScheduler.shared.syncSchedule() IntegrationService.shared.processQueueNow() |
