diff options
| author | Christian Cleberg <[email protected]> | 2026-04-26 00:22:01 -0500 |
|---|---|---|
| committer | Christian Cleberg <[email protected]> | 2026-04-26 00:22:01 -0500 |
| commit | cc69cbd7e589ec4b065ce74d8c5e7714a040cfc5 (patch) | |
| tree | 653eda7655bba21df43f4039915d452110591094 /DomainDig | |
| parent | 535bb0ff0f64d57be1074e33ceb42f598f80205b (diff) | |
| download | domain-dig-cc69cbd7e589ec4b065ce74d8c5e7714a040cfc5.tar.gz domain-dig-cc69cbd7e589ec4b065ce74d8c5e7714a040cfc5.tar.bz2 domain-dig-cc69cbd7e589ec4b065ce74d8c5e7714a040cfc5.zip | |
DomainDig v4.2.0: Add a local-only HTTP API layer for DomainDig
Expose DomainDig as a programmable local domain intelligence engine over
localhost with explicit user opt-in and token-based authentication.
Highlights:
- add a localhost-only API server with safe start/stop lifecycle
- require a local token for every request and store it in Keychain
- add read endpoints for portfolio, domains, history, events, and monitoring
- add inspection endpoints that reuse the existing inspection engine
- add monitoring enable/disable control endpoints
- add structured JSON response envelopes with API versioning
- add lightweight capped request logging with clear/reset support
- add a Settings UI for Local API enablement, token management, status, and logs
- start the server on launch only when enabled
- include Local API secrets in local data reset cleanup
This is the first programmability release for DomainDig and establishes the
foundation for Shortcuts, scripts, and other local automation workflows.
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() |
